From 235282f6f0d7d13beae19ec5614a79d1d5e5d72a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:16:21 +0900 Subject: [PATCH 01/33] add yielder --- Cargo.lock | 45 ++-- Cargo.toml | 1 + crates/yielder/Cargo.toml | 18 ++ crates/yielder/src/lib.rs | 26 +++ crates/yielder/src/provider.rs | 148 +++++++++++++ crates/yielder/src/yo/client.rs | 251 +++++++++++++++++++++++ crates/yielder/src/yo/contract.rs | 37 ++++ crates/yielder/src/yo/error.rs | 34 +++ crates/yielder/src/yo/mod.rs | 16 ++ crates/yielder/src/yo/provider.rs | 134 ++++++++++++ crates/yielder/src/yo/vault.rs | 47 +++++ gemstone/Cargo.toml | 1 + gemstone/src/gem_yielder/mod.rs | 65 ++++++ gemstone/src/gem_yielder/remote_types.rs | 75 +++++++ gemstone/src/lib.rs | 6 + 15 files changed, 889 insertions(+), 15 deletions(-) create mode 100644 crates/yielder/Cargo.toml create mode 100644 crates/yielder/src/lib.rs create mode 100644 crates/yielder/src/provider.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/error.rs create mode 100644 crates/yielder/src/yo/mod.rs create mode 100644 crates/yielder/src/yo/provider.rs create mode 100644 crates/yielder/src/yo/vault.rs create mode 100644 gemstone/src/gem_yielder/mod.rs create mode 100644 gemstone/src/gem_yielder/remote_types.rs diff --git a/Cargo.lock b/Cargo.lock index b7a61105e..a6ba511eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1703,9 +1703,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.50" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -2843,9 +2843,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "findshlibs" @@ -3625,6 +3625,7 @@ dependencies = [ "tokio", "uniffi", "url", + "yielder", "zeroize", ] @@ -5970,9 +5971,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2dc509b442812959ab125c74be2a930dd9b603038b6da9df9ec013aa23a4e9c" +checksum = "5dfe20977fe93830c0e9817a16fbf1ed1cfd8d4bba366087a1841d2c6033c251" dependencies = [ "arc-swap", "arcstr", @@ -6325,9 +6326,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +checksum = "7f5befb5191be3584a4edaf63435e8ff92ffff622e711ca7e77f8f8f365a9df8" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -6622,9 +6623,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -7043,7 +7044,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -7167,10 +7168,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -9044,6 +9046,19 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "yielder" +version = "1.0.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "async-trait", + "gem_client", + "gem_evm", + "primitives", + "serde_json", +] + [[package]] name = "yoke" version = "0.8.1" @@ -9164,6 +9179,6 @@ dependencies = [ [[package]] name = "zmij" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dccf46b25b205e4bebe1d5258a991df1cc17801017a845cb5b3fe0269781aa" +checksum = "4af59da1029247450b54ba43e0b62c8e376582464bbe5504dd525fe521e7e8fd" diff --git a/Cargo.toml b/Cargo.toml index 66ccc182d..dfc1ad76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "crates/streamer", "crates/swapper", "crates/tracing", + "crates/yielder", ] [workspace.dependencies] diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml new file mode 100644 index 000000000..ba0b0b84f --- /dev/null +++ b/crates/yielder/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "yielder" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +description.workspace = true +repository.workspace = true +documentation.workspace = true + +[dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +gem_client = { path = "../gem_client" } +gem_evm = { path = "../gem_evm", features = ["rpc"] } +primitives = { path = "../primitives" } +async-trait = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs new file mode 100644 index 000000000..e4e721fd1 --- /dev/null +++ b/crates/yielder/src/lib.rs @@ -0,0 +1,26 @@ +mod provider; +pub mod yo; + +pub use provider::{ + Yield, + YieldDepositRequest, + YieldDetails, + YieldDetailsRequest, + YieldProvider, + YieldTransaction, + YieldWithdrawRequest, + Yielder, +}; +pub use yo::{ + IYoGateway, + YoGatewayApi, + YoGatewayClient, + YoVault, + YoYieldProvider, + YieldError, + YO_GATEWAY_BASE_MAINNET, + YO_PARTNER_ID_GEM, + YO_USD, + YO_ETH, + vaults, +}; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs new file mode 100644 index 000000000..d4ca8dbb9 --- /dev/null +++ b/crates/yielder/src/provider.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use alloy_primitives::Address; +use async_trait::async_trait; +use primitives::{AssetId, Chain}; + +use crate::yo::YieldError; + +#[derive(Debug, Clone)] +pub struct Yield { + pub name: String, + pub asset: AssetId, + pub provider: String, + pub apy: Option, +} + +impl Yield { + pub fn new(name: impl Into, asset: AssetId, provider: impl Into, apy: Option) -> Self { + Self { + name: name.into(), + asset, + provider: provider.into(), + apy, + } + } +} + +#[derive(Debug, Clone)] +pub struct YieldTransaction { + pub chain: Chain, + pub from: String, + pub to: String, + pub data: String, + pub value: Option, +} + +#[derive(Debug, Clone)] +pub struct YieldDepositRequest { + pub asset: AssetId, + pub wallet_address: String, + pub receiver_address: Option, + pub amount: String, + pub min_shares: Option, + pub partner_id: Option, +} + +#[derive(Debug, Clone)] +pub struct YieldWithdrawRequest { + pub asset: AssetId, + pub wallet_address: String, + pub receiver_address: Option, + pub shares: String, + pub min_assets: Option, + pub partner_id: Option, +} + +#[derive(Debug, Clone)] +pub struct YieldDetailsRequest { + pub asset: AssetId, + pub wallet_address: String, +} + +#[derive(Debug, Clone)] +pub struct YieldDetails { + pub asset: AssetId, + pub provider: String, + pub share_token: String, + pub asset_token: String, + pub share_balance: Option, + pub asset_balance: Option, + pub rewards: Option, +} + +impl YieldDetails { + pub fn new(asset: AssetId, provider: impl Into, share_token: Address, asset_token: Address) -> Self { + Self { + asset, + provider: provider.into(), + share_token: share_token.to_string(), + asset_token: asset_token.to_string(), + share_balance: None, + asset_balance: None, + rewards: None, + } + } +} + +#[async_trait] +pub trait YieldProvider: Send + Sync { + fn protocol(&self) -> &'static str; + fn yields(&self, asset_id: &AssetId) -> Vec; + async fn deposit(&self, request: &YieldDepositRequest) -> Result; + async fn withdraw(&self, request: &YieldWithdrawRequest) -> Result; + async fn details(&self, request: &YieldDetailsRequest) -> Result; +} + +#[derive(Default)] +pub struct Yielder { + providers: Vec>, +} + +impl Yielder { + pub fn new() -> Self { + Self { providers: Vec::new() } + } + + pub fn with_providers(providers: Vec>) -> Self { + Self { providers } + } + + pub fn add_provider

(&mut self, provider: P) + where + P: YieldProvider + 'static, + { + self.providers.push(Arc::new(provider)); + } + + pub fn add_provider_arc(&mut self, provider: Arc) { + self.providers.push(provider); + } + + pub fn yields_for_asset(&self, asset_id: &AssetId) -> Vec { + self.providers.iter().flat_map(|provider| provider.yields(asset_id)).collect() + } + + pub async fn deposit(&self, protocol: &str, request: &YieldDepositRequest) -> Result { + let provider = self.provider(protocol)?; + provider.deposit(request).await + } + + pub async fn withdraw(&self, protocol: &str, request: &YieldWithdrawRequest) -> Result { + let provider = self.provider(protocol)?; + provider.withdraw(request).await + } + + pub async fn details(&self, protocol: &str, request: &YieldDetailsRequest) -> Result { + let provider = self.provider(protocol)?; + provider.details(request).await + } + + fn provider(&self, protocol: &str) -> Result, YieldError> { + self.providers + .iter() + .find(|provider| provider.protocol().eq_ignore_ascii_case(protocol)) + .cloned() + .ok_or_else(|| YieldError::new(format!("provider {protocol} not found"))) + } +} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs new file mode 100644 index 000000000..c6bda5f2f --- /dev/null +++ b/crates/yielder/src/yo/client.rs @@ -0,0 +1,251 @@ +use alloy_primitives::{Address, U256, hex}; +use alloy_sol_types::SolCall; +use async_trait::async_trait; +use gem_client::Client; +use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; +use primitives::Chain; +use serde_json::json; + +use super::{contract::IYoGateway, error::YieldError, YoVault, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM}; + +#[async_trait] +pub trait YoGatewayApi: Send + Sync { + fn contract_address(&self) -> Address; + fn chain(&self) -> Chain; + fn build_deposit_transaction( + &self, + from: Address, + yo_vault: Address, + assets: U256, + min_shares_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject; + fn build_redeem_transaction( + &self, + from: Address, + yo_vault: Address, + shares: U256, + min_assets_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject; + async fn balance_of(&self, token: Address, owner: Address) -> Result; +} + +#[derive(Debug, Clone)] +pub struct YoGatewayClient { + ethereum_client: EthereumClient, + contract_address: Address, +} + +impl YoGatewayClient { + pub const fn default_partner_id() -> u32 { + YO_PARTNER_ID_GEM + } + + pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { + Self { ethereum_client, contract_address } + } + + pub fn base_mainnet(ethereum_client: EthereumClient) -> Self { + Self::new(ethereum_client, YO_GATEWAY_BASE_MAINNET) + } + + pub fn contract_address(&self) -> Address { + self.contract_address + } + + pub async fn quote_convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { + self.call_contract(IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }).await + } + + pub async fn quote_convert_to_assets(&self, yo_vault: Address, shares: U256) -> Result { + self.call_contract(IYoGateway::quoteConvertToAssetsCall { yoVault: yo_vault, shares }).await + } + + pub async fn quote_preview_deposit(&self, yo_vault: Address, assets: U256) -> Result { + self.call_contract(IYoGateway::quotePreviewDepositCall { yoVault: yo_vault, assets }).await + } + + pub async fn quote_preview_redeem(&self, yo_vault: Address, shares: U256) -> Result { + self.call_contract(IYoGateway::quotePreviewRedeemCall { yoVault: yo_vault, shares }).await + } + + pub async fn get_asset_allowance(&self, yo_vault: Address, owner: Address) -> Result { + self.call_contract(IYoGateway::getAssetAllowanceCall { yoVault: yo_vault, owner }).await + } + + pub async fn get_share_allowance(&self, yo_vault: Address, owner: Address) -> Result { + self.call_contract(IYoGateway::getShareAllowanceCall { yoVault: yo_vault, owner }).await + } + + pub async fn quote_convert_to_shares_for(&self, vault: YoVault, assets: U256) -> Result { + self.quote_convert_to_shares(vault.yo_token, assets).await + } + + pub async fn quote_convert_to_assets_for(&self, vault: YoVault, shares: U256) -> Result { + self.quote_convert_to_assets(vault.yo_token, shares).await + } + + pub async fn quote_preview_deposit_for(&self, vault: YoVault, assets: U256) -> Result { + self.quote_preview_deposit(vault.yo_token, assets).await + } + + pub async fn quote_preview_redeem_for(&self, vault: YoVault, shares: U256) -> Result { + self.quote_preview_redeem(vault.yo_token, shares).await + } + + pub async fn get_asset_allowance_for(&self, vault: YoVault, owner: Address) -> Result { + self.get_asset_allowance(vault.yo_token, owner).await + } + + pub async fn get_share_allowance_for(&self, vault: YoVault, owner: Address) -> Result { + self.get_share_allowance(vault.yo_token, owner).await + } + + pub fn deposit_call_data(yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { + IYoGateway::depositCall { + yoVault: yo_vault, + assets, + minSharesOut: min_shares_out, + receiver, + partnerId: partner_id, + } + .abi_encode() + } + + pub fn redeem_call_data(yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { + IYoGateway::redeemCall { + yoVault: yo_vault, + shares, + minAssetsOut: min_assets_out, + receiver, + partnerId: partner_id, + } + .abi_encode() + } + + pub fn deposit_call_data_for(vault: YoVault, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { + Self::deposit_call_data(vault.yo_token, assets, min_shares_out, receiver, partner_id) + } + + pub fn redeem_call_data_for(vault: YoVault, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { + Self::redeem_call_data(vault.yo_token, shares, min_assets_out, receiver, partner_id) + } + + pub fn build_deposit_transaction( + &self, + from: Address, + yo_vault: Address, + assets: U256, + min_shares_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject { + let data = Self::deposit_call_data(yo_vault, assets, min_shares_out, receiver, partner_id); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) + } + + pub fn build_redeem_transaction( + &self, + from: Address, + yo_vault: Address, + shares: U256, + min_assets_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject { + let data = Self::redeem_call_data(yo_vault, shares, min_assets_out, receiver, partner_id); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) + } + + async fn call_contract(&self, call: Call) -> Result + where + Call: SolCall, + { + let encoded = call.abi_encode(); + let payload = hex::encode_prefixed(&encoded); + let contract = self.contract_address.to_string(); + let response: String = self + .ethereum_client + .eth_call(&contract, &payload) + .await + .map_err(|err| YieldError::new(format!("yo gateway rpc call failed: {err}")))?; + + if response.trim().is_empty() || response == "0x" { + return Err(YieldError::new("yo gateway response did not contain data")); + } + + let decoded = hex::decode(&response) + .map_err(|err| YieldError::new(format!("invalid hex returned by yo gateway: {err}")))?; + Call::abi_decode_returns(&decoded) + .map_err(|err| YieldError::new(format!("failed to decode yo gateway response: {err}"))) + } +} + +#[async_trait] +impl YoGatewayApi for YoGatewayClient +where + C: Client + Clone + Send + Sync + 'static, +{ + fn contract_address(&self) -> Address { + self.contract_address + } + + fn chain(&self) -> Chain { + self.ethereum_client.get_chain() + } + + fn build_deposit_transaction( + &self, + from: Address, + yo_vault: Address, + assets: U256, + min_shares_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject { + >::build_deposit_transaction(self, from, yo_vault, assets, min_shares_out, receiver, partner_id) + } + + fn build_redeem_transaction( + &self, + from: Address, + yo_vault: Address, + shares: U256, + min_assets_out: U256, + receiver: Address, + partner_id: u32, + ) -> TransactionObject { + >::build_redeem_transaction(self, from, yo_vault, shares, min_assets_out, receiver, partner_id) + } + + async fn balance_of(&self, token: Address, owner: Address) -> Result { + alloy_sol_types::sol! { + interface IERC20Balance { + function balanceOf(address account) external view returns (uint256); + } + } + + let call = IERC20Balance::balanceOfCall { account: owner }.abi_encode(); + let payload = hex::encode_prefixed(call); + let params = json!([ + { + "to": token.to_string(), + "data": payload, + }, + "latest" + ]); + + let result: String = self + .ethereum_client + .client + .call("eth_call", params) + .await + .map_err(|err| YieldError::new(format!("yo gateway rpc call failed: {err}")))?; + + let value = result.trim_start_matches("0x"); + U256::from_str_radix(value, 16).map_err(|err| YieldError::new(format!("invalid balance data: {err}"))) + } +} diff --git a/crates/yielder/src/yo/contract.rs b/crates/yielder/src/yo/contract.rs new file mode 100644 index 000000000..227393ce9 --- /dev/null +++ b/crates/yielder/src/yo/contract.rs @@ -0,0 +1,37 @@ +use alloy_sol_types::sol; + +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/error.rs b/crates/yielder/src/yo/error.rs new file mode 100644 index 000000000..bea72f6c1 --- /dev/null +++ b/crates/yielder/src/yo/error.rs @@ -0,0 +1,34 @@ +use std::{error::Error, fmt}; + +#[derive(Debug, Clone)] +pub struct YieldError(String); + +impl YieldError { + pub fn new(message: impl Into) -> Self { + Self(message.into()) + } + + pub fn message(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for YieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for YieldError {} + +impl From<&str> for YieldError { + fn from(value: &str) -> Self { + YieldError::new(value) + } +} + +impl From for YieldError { + fn from(value: String) -> Self { + YieldError::new(value) + } +} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs new file mode 100644 index 000000000..9d6a57a8e --- /dev/null +++ b/crates/yielder/src/yo/mod.rs @@ -0,0 +1,16 @@ +mod client; +mod contract; +mod error; +mod provider; +mod vault; + +pub use client::{YoGatewayApi, YoGatewayClient}; +pub use contract::IYoGateway; +pub use error::YieldError; +pub use provider::YoYieldProvider; +pub use vault::{vaults, YoVault, YO_ETH, YO_USD}; + +use alloy_primitives::{address, Address}; + +pub const YO_GATEWAY_BASE_MAINNET: 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..24ae130d6 --- /dev/null +++ b/crates/yielder/src/yo/provider.rs @@ -0,0 +1,134 @@ +use std::{str::FromStr, sync::Arc}; + +use alloy_primitives::{Address, U256}; +use async_trait::async_trait; +use gem_evm::jsonrpc::TransactionObject; +use primitives::AssetId; + +use crate::provider::{ + Yield, + YieldDepositRequest, + YieldDetails, + YieldDetailsRequest, + YieldProvider, + YieldTransaction, + YieldWithdrawRequest, +}; + +use super::{ + client::YoGatewayApi, + error::YieldError, + vaults, + YoVault, + YO_PARTNER_ID_GEM, +}; + +#[derive(Clone)] +pub struct YoYieldProvider { + vaults: Vec, + gateway: Arc, +} + +impl YoYieldProvider { + pub fn new(gateway: Arc) -> Self { + Self { + vaults: vaults().to_vec(), + gateway, + } + } + + fn find_vault(&self, asset_id: &AssetId) -> Result { + self.vaults + .iter() + .copied() + .find(|vault| vault.asset_id() == *asset_id) + .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) + } +} + +#[async_trait] +impl YieldProvider for YoYieldProvider { + fn protocol(&self) -> &'static str { + "yo" + } + + fn yields(&self, asset_id: &AssetId) -> Vec { + self.vaults + .iter() + .filter_map(|vault| { + let vault_asset = vault.asset_id(); + if &vault_asset == asset_id { + Some(Yield::new(vault.name, vault_asset, self.protocol(), None)) + } else { + None + } + }) + .collect() + } + + async fn deposit(&self, request: &YieldDepositRequest) -> Result { + let vault = self.find_vault(&request.asset)?; + let wallet = parse_address(&request.wallet_address)?; + let receiver = match &request.receiver_address { + Some(address) => parse_address(address)?, + None => wallet, + }; + let amount = parse_amount(&request.amount)?; + let min_shares = parse_amount(request.min_shares.as_deref().unwrap_or("0"))?; + let partner_id = request.partner_id.unwrap_or(YO_PARTNER_ID_GEM); + + let tx = self + .gateway + .build_deposit_transaction(wallet, vault.yo_token, amount, min_shares, receiver, partner_id); + Ok(convert_transaction(vault, tx)) + } + + async fn withdraw(&self, request: &YieldWithdrawRequest) -> Result { + let vault = self.find_vault(&request.asset)?; + let wallet = parse_address(&request.wallet_address)?; + let receiver = match &request.receiver_address { + Some(address) => parse_address(address)?, + None => wallet, + }; + let shares = parse_amount(&request.shares)?; + let min_assets = parse_amount(request.min_assets.as_deref().unwrap_or("0"))?; + let partner_id = request.partner_id.unwrap_or(YO_PARTNER_ID_GEM); + + let tx = self + .gateway + .build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); + Ok(convert_transaction(vault, tx)) + } + + async fn details(&self, request: &YieldDetailsRequest) -> Result { + let vault = self.find_vault(&request.asset)?; + let owner = parse_address(&request.wallet_address)?; + let mut details = YieldDetails::new(request.asset.clone(), self.protocol(), vault.yo_token, vault.asset_token); + + let share_balance = self.gateway.balance_of(vault.yo_token, owner).await?; + details.share_balance = Some(share_balance.to_string()); + + let asset_balance = self.gateway.balance_of(vault.asset_token, owner).await?; + details.asset_balance = Some(asset_balance.to_string()); + + Ok(details) + } +} + +fn parse_address(value: &str) -> Result { + Address::from_str(value).map_err(|err| YieldError::new(format!("invalid address {value}: {err}"))) +} + +fn parse_amount(value: &str) -> Result { + U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid amount {value}: {err}"))) +} + +fn convert_transaction(vault: YoVault, tx: TransactionObject) -> YieldTransaction { + YieldTransaction { + chain: vault.chain, + from: tx.from.unwrap_or_default(), + to: tx.to, + data: tx.data, + value: tx.value, + } +} diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs new file mode 100644 index 000000000..d5f0a82de --- /dev/null +++ b/crates/yielder/src/yo/vault.rs @@ -0,0 +1,47 @@ +use alloy_primitives::{address, Address}; +use primitives::{AssetId, Chain}; + +#[derive(Debug, Clone, Copy)] +pub struct YoVault { + pub name: &'static str, + pub chain: Chain, + pub yo_token: Address, + pub asset_token: Address, + pub asset_decimals: u8, +} + +impl YoVault { + pub const fn new(name: &'static str, chain: Chain, yo_token: Address, asset_token: Address, asset_decimals: u8) -> Self { + Self { + name, + chain, + yo_token, + asset_token, + asset_decimals, + } + } + + pub fn asset_id(&self) -> AssetId { + AssetId::from_token(self.chain, &self.asset_token.to_string()) + } +} + +pub const YO_USD: YoVault = YoVault::new( + "yoUSD", + Chain::Base, + address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), + address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), + 6, +); + +pub const YO_ETH: YoVault = YoVault::new( + "yoETH", + Chain::Base, + address!("0x3a43aec53490cb9fa922847385d82fe25d0e9de7"), + address!("0x4200000000000000000000000000000000000006"), + 18, +); + +pub fn vaults() -> &'static [YoVault] { + &[YO_USD, YO_ETH] +} diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 9e8345d35..41f7361db 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -19,6 +19,7 @@ swap_integration_tests = ["reqwest_provider"] [dependencies] swapper = { path = "../crates/swapper" } +yielder = { path = "../crates/yielder" } primitives = { path = "../crates/primitives" } gem_cosmos = { path = "../crates/gem_cosmos", features = ["rpc"] } gem_solana = { path = "../crates/gem_solana", features = ["rpc"] } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs new file mode 100644 index 000000000..4605ffff6 --- /dev/null +++ b/gemstone/src/gem_yielder/mod.rs @@ -0,0 +1,65 @@ +mod remote_types; +pub use remote_types::*; + +use std::sync::Arc; + +use crate::{ + alien::{AlienProvider, AlienProviderWrapper}, + GemstoneError, +}; +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use gem_jsonrpc::rpc::RpcClient; +use primitives::{AssetId, Chain, EVMChain}; +use yielder::{YieldProvider, YoGatewayApi, YoGatewayClient, YoYieldProvider, Yielder, YO_GATEWAY_BASE_MAINNET}; + +#[derive(uniffi::Object)] +pub struct GemYielder { + inner: Yielder, +} + +impl std::fmt::Debug for GemYielder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GemYielder").finish() + } +} + +#[uniffi::export] +impl GemYielder { + #[uniffi::constructor] + pub fn new(rpc_provider: Arc) -> Result { + let mut inner = Yielder::new(); + let yo_provider = build_yo_provider(rpc_provider)?; + inner.add_provider_arc(yo_provider); + Ok(Self { inner }) + } + + pub fn yields_for_asset(&self, asset_id: &AssetId) -> Vec { + self.inner.yields_for_asset(asset_id) + } + + pub async fn deposit(&self, provider: String, request: GemYieldDepositRequest) -> Result { + self.inner.deposit(&provider, &request).await.map_err(Into::into) + } + + pub async fn withdraw(&self, provider: String, request: GemYieldWithdrawRequest) -> Result { + self.inner.withdraw(&provider, &request).await.map_err(Into::into) + } + + pub async fn details(&self, provider: String, request: GemYieldDetailsRequest) -> Result { + self.inner.details(&provider, &request).await.map_err(Into::into) + } +} + +fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { + let endpoint = rpc_provider.get_endpoint(Chain::Base)?; + let wrapper = AlienProviderWrapper { provider: rpc_provider }; + let rpc_client = RpcClient::new(endpoint, Arc::new(wrapper)); + let jsonrpc_client = JsonRpcClient::new(rpc_client); + let evm_chain = EVMChain::Base; + let ethereum_client = EthereumClient::new(jsonrpc_client, evm_chain); + let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); + let gateway: Arc = Arc::new(gateway_client); + let provider: Arc = Arc::new(YoYieldProvider::new(gateway)); + Ok(provider) +} diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs new file mode 100644 index 000000000..82de38bb0 --- /dev/null +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -0,0 +1,75 @@ +use primitives::AssetId; +use yielder::{ + Yield as CoreYield, + YieldDepositRequest as CoreDepositRequest, + YieldDetails as CoreDetails, + YieldDetailsRequest as CoreDetailsRequest, + YieldTransaction as CoreTransaction, + YieldWithdrawRequest as CoreWithdrawRequest, +}; + +pub type GemYield = CoreYield; + +#[uniffi::remote(Record)] +pub struct GemYield { + pub name: String, + pub asset: AssetId, + pub provider: String, + pub apy: Option, +} + +pub type GemYieldTransaction = CoreTransaction; + +#[uniffi::remote(Record)] +pub struct GemYieldTransaction { + pub chain: primitives::Chain, + pub from: String, + pub to: String, + pub data: String, + pub value: Option, +} + +pub type GemYieldDepositRequest = CoreDepositRequest; + +#[uniffi::remote(Record)] +pub struct GemYieldDepositRequest { + pub asset: AssetId, + pub wallet_address: String, + pub receiver_address: Option, + pub amount: String, + pub min_shares: Option, + pub partner_id: Option, +} + +pub type GemYieldWithdrawRequest = CoreWithdrawRequest; + +#[uniffi::remote(Record)] +pub struct GemYieldWithdrawRequest { + pub asset: AssetId, + pub wallet_address: String, + pub receiver_address: Option, + pub shares: String, + pub min_assets: Option, + pub partner_id: Option, +} + +pub type GemYieldDetailsRequest = CoreDetailsRequest; + +#[uniffi::remote(Record)] +pub struct GemYieldDetailsRequest { + pub asset: AssetId, + pub wallet_address: String, +} + +pub type GemYieldDetails = CoreDetails; + +#[uniffi::remote(Record)] +pub struct GemYieldDetails { + pub asset: AssetId, + pub provider: String, + pub share_token: String, + pub asset_token: String, + pub share_balance: Option, + pub asset_balance: Option, + pub rewards: Option, +} diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index 3fef0361a..e20d77414 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -6,6 +6,7 @@ pub mod config; pub mod ethereum; pub mod gateway; pub mod gem_swapper; +pub mod gem_yielder; pub mod message; pub mod models; pub mod network; @@ -106,3 +107,8 @@ impl From for GemstoneError { Self::AnyError { msg: error.to_string() } } } +impl From for GemstoneError { + fn from(error: yielder::yo::YieldError) -> Self { + Self::AnyError { msg: error.to_string() } + } +} From c0f52769d6609cede89ee3350f57cb31539fd7fb Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:53:36 +0900 Subject: [PATCH 02/33] add apy --- Cargo.lock | 3 + crates/yielder/Cargo.toml | 11 ++ crates/yielder/src/lib.rs | 26 +--- crates/yielder/src/provider.rs | 46 +++--- crates/yielder/src/yield_integration_tests.rs | 39 +++++ crates/yielder/src/yo/client.rs | 100 ++++++++++--- crates/yielder/src/yo/mod.rs | 4 +- crates/yielder/src/yo/provider.rs | 134 +++++++++++++----- crates/yielder/src/yo/vault.rs | 2 +- gemstone/src/gem_yielder/mod.rs | 19 +-- gemstone/src/gem_yielder/remote_types.rs | 42 +----- 11 files changed, 273 insertions(+), 153 deletions(-) create mode 100644 crates/yielder/src/yield_integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index a6ba511eb..a00321677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9055,8 +9055,11 @@ dependencies = [ "async-trait", "gem_client", "gem_evm", + "gem_jsonrpc", "primitives", + "reqwest", "serde_json", + "tokio", ] [[package]] diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index ba0b0b84f..579e1eb64 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -8,6 +8,10 @@ description.workspace = true repository.workspace = true documentation.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 } @@ -16,3 +20,10 @@ gem_evm = { path = "../gem_evm", features = ["rpc"] } primitives = { path = "../primitives" } async-trait = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros"] } + +[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/lib.rs b/crates/yielder/src/lib.rs index e4e721fd1..0f90082bb 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -1,26 +1,10 @@ mod provider; pub mod yo; -pub use provider::{ - Yield, - YieldDepositRequest, - YieldDetails, - YieldDetailsRequest, - YieldProvider, - YieldTransaction, - YieldWithdrawRequest, - Yielder, -}; +pub use provider::{Yield, YieldDetails, YieldDetailsRequest, YieldProvider, YieldTransaction, Yielder}; pub use yo::{ - IYoGateway, - YoGatewayApi, - YoGatewayClient, - YoVault, - YoYieldProvider, - YieldError, - YO_GATEWAY_BASE_MAINNET, - YO_PARTNER_ID_GEM, - YO_USD, - YO_ETH, - vaults, + IYoGateway, YO_ETH, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YO_USD, YieldError, YoGatewayApi, YoGatewayClient, YoVault, YoYieldProvider, vaults, }; + +#[cfg(all(test, feature = "yield_integration_tests"))] +mod yield_integration_tests; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index d4ca8dbb9..b7ff7d220 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -34,26 +34,6 @@ pub struct YieldTransaction { pub value: Option, } -#[derive(Debug, Clone)] -pub struct YieldDepositRequest { - pub asset: AssetId, - pub wallet_address: String, - pub receiver_address: Option, - pub amount: String, - pub min_shares: Option, - pub partner_id: Option, -} - -#[derive(Debug, Clone)] -pub struct YieldWithdrawRequest { - pub asset: AssetId, - pub wallet_address: String, - pub receiver_address: Option, - pub shares: String, - pub min_assets: Option, - pub partner_id: Option, -} - #[derive(Debug, Clone)] pub struct YieldDetailsRequest { pub asset: AssetId, @@ -68,6 +48,7 @@ pub struct YieldDetails { pub asset_token: String, pub share_balance: Option, pub asset_balance: Option, + pub apy: Option, pub rewards: Option, } @@ -80,6 +61,7 @@ impl YieldDetails { asset_token: asset_token.to_string(), share_balance: None, asset_balance: None, + apy: None, rewards: None, } } @@ -89,9 +71,12 @@ impl YieldDetails { pub trait YieldProvider: Send + Sync { fn protocol(&self) -> &'static str; fn yields(&self, asset_id: &AssetId) -> Vec; - async fn deposit(&self, request: &YieldDepositRequest) -> Result; - async fn withdraw(&self, request: &YieldWithdrawRequest) -> Result; + async fn deposit(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result; + async fn withdraw(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result; async fn details(&self, request: &YieldDetailsRequest) -> Result; + async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { + Ok(self.yields(asset_id)) + } } #[derive(Default)] @@ -123,14 +108,23 @@ impl Yielder { self.providers.iter().flat_map(|provider| provider.yields(asset_id)).collect() } - pub async fn deposit(&self, protocol: &str, request: &YieldDepositRequest) -> Result { + pub async fn yields_for_asset_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { + let mut yields = Vec::new(); + for provider in &self.providers { + let mut provider_yields = provider.yields_with_apy(asset_id).await?; + yields.append(&mut provider_yields); + } + Ok(yields) + } + + pub async fn deposit(&self, protocol: &str, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { let provider = self.provider(protocol)?; - provider.deposit(request).await + provider.deposit(asset, wallet_address, amount).await } - pub async fn withdraw(&self, protocol: &str, request: &YieldWithdrawRequest) -> Result { + pub async fn withdraw(&self, protocol: &str, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { let provider = self.provider(protocol)?; - provider.withdraw(request).await + provider.withdraw(asset, wallet_address, amount).await } pub async fn details(&self, protocol: &str, request: &YieldDetailsRequest) -> Result { diff --git a/crates/yielder/src/yield_integration_tests.rs b/crates/yielder/src/yield_integration_tests.rs new file mode 100644 index 000000000..608276a65 --- /dev/null +++ b/crates/yielder/src/yield_integration_tests.rs @@ -0,0 +1,39 @@ +#![cfg(all(test, feature = "yield_integration_tests"))] + +use std::sync::Arc; + +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::EVMChain; + +use crate::{YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, Yielder, YoGatewayClient, YoYieldProvider}; + +#[tokio::test] +async fn yield_integration_test_fetches_performance_apy() -> Result<(), Box> { + let rpc_url = std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://mainnet.base.org".to_string()); + let jsonrpc_client = JsonRpcClient::new_reqwest(rpc_url); + let ethereum_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); + let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); + let provider: Arc = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); + let yielder = Yielder::with_providers(vec![provider]); + + let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; + assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); + let apy = apy_yields[0].apy.expect("apy should be computed"); + assert!(apy.is_finite(), "apy should be finite"); + assert!(apy > -1.0, "apy should be > -100%"); + + let details = yielder + .details( + "yo", + &YieldDetailsRequest { + asset: YO_USD.asset_id(), + wallet_address: "0x0000000000000000000000000000000000000000".to_string(), + }, + ) + .await?; + + assert!(details.apy.is_some(), "apy should be present in details"); + + Ok(()) +} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index c6bda5f2f..485e77d47 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -6,7 +6,13 @@ use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; use primitives::Chain; use serde_json::json; -use super::{contract::IYoGateway, error::YieldError, YoVault, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM}; +use super::{YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YoVault, contract::IYoGateway, error::YieldError}; + +alloy_sol_types::sol! { + interface IYoVaultToken { + function convertToAssets(uint256 shares) external view returns (uint256 assets); + } +} #[async_trait] pub trait YoGatewayApi: Send + Sync { @@ -31,6 +37,9 @@ pub trait YoGatewayApi: Send + Sync { partner_id: u32, ) -> TransactionObject; async fn balance_of(&self, token: Address, owner: Address) -> Result; + async fn convert_to_assets_at_block(&self, yo_vault: Address, shares: U256, block_number: u64) -> Result; + async fn latest_block_number(&self) -> Result; + async fn block_timestamp(&self, block_number: u64) -> Result; } #[derive(Debug, Clone)] @@ -45,7 +54,10 @@ impl YoGatewayClient { } pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { - Self { ethereum_client, contract_address } + Self { + ethereum_client, + contract_address, + } } pub fn base_mainnet(ethereum_client: EthereumClient) -> Self { @@ -57,27 +69,31 @@ impl YoGatewayClient { } pub async fn quote_convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { - self.call_contract(IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }).await + self.call_gateway_contract(IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }) + .await } pub async fn quote_convert_to_assets(&self, yo_vault: Address, shares: U256) -> Result { - self.call_contract(IYoGateway::quoteConvertToAssetsCall { yoVault: yo_vault, shares }).await + self.call_gateway_contract(IYoGateway::quoteConvertToAssetsCall { yoVault: yo_vault, shares }) + .await } pub async fn quote_preview_deposit(&self, yo_vault: Address, assets: U256) -> Result { - self.call_contract(IYoGateway::quotePreviewDepositCall { yoVault: yo_vault, assets }).await + self.call_gateway_contract(IYoGateway::quotePreviewDepositCall { yoVault: yo_vault, assets }) + .await } pub async fn quote_preview_redeem(&self, yo_vault: Address, shares: U256) -> Result { - self.call_contract(IYoGateway::quotePreviewRedeemCall { yoVault: yo_vault, shares }).await + self.call_gateway_contract(IYoGateway::quotePreviewRedeemCall { yoVault: yo_vault, shares }) + .await } pub async fn get_asset_allowance(&self, yo_vault: Address, owner: Address) -> Result { - self.call_contract(IYoGateway::getAssetAllowanceCall { yoVault: yo_vault, owner }).await + self.call_gateway_contract(IYoGateway::getAssetAllowanceCall { yoVault: yo_vault, owner }).await } pub async fn get_share_allowance(&self, yo_vault: Address, owner: Address) -> Result { - self.call_contract(IYoGateway::getShareAllowanceCall { yoVault: yo_vault, owner }).await + self.call_gateway_contract(IYoGateway::getShareAllowanceCall { yoVault: yo_vault, owner }).await } pub async fn quote_convert_to_shares_for(&self, vault: YoVault, assets: U256) -> Result { @@ -160,16 +176,37 @@ impl YoGatewayClient { TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn call_contract(&self, call: Call) -> Result + async fn call_gateway_contract(&self, call: Call) -> Result where Call: SolCall, { - let encoded = call.abi_encode(); - let payload = hex::encode_prefixed(&encoded); - let contract = self.contract_address.to_string(); + self.call_contract_at_block(call, self.contract_address, None).await + } + + async fn call_contract_at_block(&self, call: Call, contract: Address, block_number: Option) -> Result + where + Call: SolCall, + { + let payload = hex::encode_prefixed(call.abi_encode()); + let contract_address = contract.to_string(); + + let block_param = block_number + .map(|number| format!("0x{number:x}")) + .map_or_else(|| json!("latest"), serde_json::Value::String); + let response: String = self .ethereum_client - .eth_call(&contract, &payload) + .client + .call( + "eth_call", + json!([ + { + "to": contract_address, + "data": payload, + }, + block_param + ]), + ) .await .map_err(|err| YieldError::new(format!("yo gateway rpc call failed: {err}")))?; @@ -177,10 +214,8 @@ impl YoGatewayClient { return Err(YieldError::new("yo gateway response did not contain data")); } - let decoded = hex::decode(&response) - .map_err(|err| YieldError::new(format!("invalid hex returned by yo gateway: {err}")))?; - Call::abi_decode_returns(&decoded) - .map_err(|err| YieldError::new(format!("failed to decode yo gateway response: {err}"))) + let decoded = hex::decode(&response).map_err(|err| YieldError::new(format!("invalid hex returned by yo gateway: {err}")))?; + Call::abi_decode_returns(&decoded).map_err(|err| YieldError::new(format!("failed to decode yo gateway response: {err}"))) } } @@ -248,4 +283,35 @@ where let value = result.trim_start_matches("0x"); U256::from_str_radix(value, 16).map_err(|err| YieldError::new(format!("invalid balance data: {err}"))) } + + async fn convert_to_assets_at_block(&self, yo_vault: Address, shares: U256, block_number: u64) -> Result { + self.call_contract_at_block(IYoVaultToken::convertToAssetsCall { shares }, yo_vault, Some(block_number)) + .await + } + + async fn latest_block_number(&self) -> Result { + self.ethereum_client + .get_latest_block() + .await + .map_err(|err| YieldError::new(format!("yo gateway failed to fetch latest block: {err}"))) + } + + async fn block_timestamp(&self, block_number: u64) -> Result { + let block_hex = format!("0x{block_number:x}"); + let mut blocks = self + .ethereum_client + .get_blocks(&[block_hex], false) + .await + .map_err(|err| YieldError::new(format!("yo gateway failed to fetch block {block_number}: {err}")))?; + + let block = blocks + .pop() + .ok_or_else(|| YieldError::new(format!("yo gateway missing block data for {block_number}")))?; + + block + .timestamp + .to_string() + .parse::() + .map_err(|err| YieldError::new(format!("yo gateway failed to parse timestamp for block {block_number}: {err}"))) + } } diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 9d6a57a8e..a11eac4a3 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -8,9 +8,9 @@ pub use client::{YoGatewayApi, YoGatewayClient}; pub use contract::IYoGateway; pub use error::YieldError; pub use provider::YoYieldProvider; -pub use vault::{vaults, YoVault, YO_ETH, YO_USD}; +pub use vault::{YO_ETH, YO_USD, YoVault, vaults}; -use alloy_primitives::{address, Address}; +use alloy_primitives::{Address, address}; pub const YO_GATEWAY_BASE_MAINNET: 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 index 24ae130d6..8e2a3b8bf 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -4,24 +4,14 @@ use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; use primitives::AssetId; +use tokio::try_join; -use crate::provider::{ - Yield, - YieldDepositRequest, - YieldDetails, - YieldDetailsRequest, - YieldProvider, - YieldTransaction, - YieldWithdrawRequest, -}; - -use super::{ - client::YoGatewayApi, - error::YieldError, - vaults, - YoVault, - YO_PARTNER_ID_GEM, -}; +use crate::provider::{Yield, YieldDetails, YieldDetailsRequest, YieldProvider, YieldTransaction}; + +use super::{YO_PARTNER_ID_GEM, YoVault, client::YoGatewayApi, error::YieldError, vaults}; + +const SECONDS_PER_YEAR: f64 = 31_536_000.0; +const APY_LOOKBACK_SECONDS: u64 = 7 * 24 * 60 * 60; #[derive(Clone)] pub struct YoYieldProvider { @@ -44,6 +34,46 @@ impl YoYieldProvider { .find(|vault| vault.asset_id() == *asset_id) .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) } + + async fn performance_apy(&self, vault: YoVault) -> Result, YieldError> { + let latest_block = self.gateway.latest_block_number().await?; + let latest_timestamp = self.gateway.block_timestamp(latest_block).await?; + let target_timestamp = latest_timestamp.saturating_sub(APY_LOOKBACK_SECONDS); + let lookback_block = self.find_block_before(target_timestamp, latest_block).await?; + let (latest_price, lookback_price) = try_join!(self.share_price_at_block(vault, latest_block), self.share_price_at_block(vault, lookback_block))?; + let lookback_timestamp = self.gateway.block_timestamp(lookback_block).await?; + let elapsed = latest_timestamp.saturating_sub(lookback_timestamp); + Ok(annualize_growth(latest_price, lookback_price, elapsed)) + } + + async fn share_price_at_block(&self, vault: YoVault, block_number: u64) -> Result { + let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); + self.gateway.convert_to_assets_at_block(vault.yo_token, one_share, block_number).await + } + + async fn find_block_before(&self, target_timestamp: u64, latest_block: u64) -> Result { + let mut low = 0; + let mut high = latest_block; + let mut candidate = latest_block; + + while low <= high { + let mid = (low + high) / 2; + let mid_timestamp = self.gateway.block_timestamp(mid).await?; + + if mid_timestamp > target_timestamp { + if mid == 0 { + candidate = 0; + break; + } + high = mid - 1; + } else { + candidate = mid; + low = mid + 1; + } + } + + Ok(candidate) + } } #[async_trait] @@ -66,16 +96,24 @@ impl YieldProvider for YoYieldProvider { .collect() } - async fn deposit(&self, request: &YieldDepositRequest) -> Result { - let vault = self.find_vault(&request.asset)?; - let wallet = parse_address(&request.wallet_address)?; - let receiver = match &request.receiver_address { - Some(address) => parse_address(address)?, - None => wallet, - }; - let amount = parse_amount(&request.amount)?; - let min_shares = parse_amount(request.min_shares.as_deref().unwrap_or("0"))?; - let partner_id = request.partner_id.unwrap_or(YO_PARTNER_ID_GEM); + async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { + let mut results = Vec::new(); + + for vault in self.vaults.iter().copied().filter(|vault| vault.asset_id() == *asset_id) { + let apy = self.performance_apy(vault).await?; + results.push(Yield::new(vault.name, vault.asset_id(), self.protocol(), apy)); + } + + Ok(results) + } + + async fn deposit(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { + let vault = self.find_vault(asset)?; + let wallet = parse_address(wallet_address)?; + let receiver = wallet; + let amount = parse_amount(amount)?; + let min_shares = U256::from(0); + let partner_id = YO_PARTNER_ID_GEM; let tx = self .gateway @@ -83,16 +121,13 @@ impl YieldProvider for YoYieldProvider { Ok(convert_transaction(vault, tx)) } - async fn withdraw(&self, request: &YieldWithdrawRequest) -> Result { - let vault = self.find_vault(&request.asset)?; - let wallet = parse_address(&request.wallet_address)?; - let receiver = match &request.receiver_address { - Some(address) => parse_address(address)?, - None => wallet, - }; - let shares = parse_amount(&request.shares)?; - let min_assets = parse_amount(request.min_assets.as_deref().unwrap_or("0"))?; - let partner_id = request.partner_id.unwrap_or(YO_PARTNER_ID_GEM); + async fn withdraw(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { + let vault = self.find_vault(asset)?; + let wallet = parse_address(wallet_address)?; + let receiver = wallet; + let shares = parse_amount(amount)?; + let min_assets = U256::from(0); + let partner_id = YO_PARTNER_ID_GEM; let tx = self .gateway @@ -111,6 +146,8 @@ impl YieldProvider for YoYieldProvider { let asset_balance = self.gateway.balance_of(vault.asset_token, owner).await?; details.asset_balance = Some(asset_balance.to_string()); + details.apy = self.performance_apy(vault).await?; + Ok(details) } } @@ -132,3 +169,26 @@ fn convert_transaction(vault: YoVault, tx: TransactionObject) -> YieldTransactio value: tx.value, } } + +fn annualize_growth(latest_assets: U256, previous_assets: U256, elapsed_seconds: u64) -> Option { + if elapsed_seconds == 0 || previous_assets.is_zero() { + return None; + } + + let latest = u256_to_f64(latest_assets)?; + let previous = u256_to_f64(previous_assets)?; + if latest <= 0.0 || previous <= 0.0 { + return None; + } + + let growth = latest / previous; + if !growth.is_finite() || growth <= 0.0 { + return None; + } + + Some(growth.powf(SECONDS_PER_YEAR / elapsed_seconds as f64) - 1.0) +} + +fn u256_to_f64(value: U256) -> Option { + value.to_string().parse::().ok() +} diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index d5f0a82de..cb327b5ea 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{address, Address}; +use alloy_primitives::{Address, address}; use primitives::{AssetId, Chain}; #[derive(Debug, Clone, Copy)] diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 4605ffff6..023dd4e0f 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -4,14 +4,14 @@ pub use remote_types::*; use std::sync::Arc; use crate::{ - alien::{AlienProvider, AlienProviderWrapper}, GemstoneError, + alien::{AlienProvider, AlienProviderWrapper}, }; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{YieldProvider, YoGatewayApi, YoGatewayClient, YoYieldProvider, Yielder, YO_GATEWAY_BASE_MAINNET}; +use yielder::{YO_GATEWAY_BASE_MAINNET, YieldDetailsRequest, YieldProvider, Yielder, YoGatewayApi, YoGatewayClient, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -34,19 +34,20 @@ impl GemYielder { Ok(Self { inner }) } - pub fn yields_for_asset(&self, asset_id: &AssetId) -> Vec { - self.inner.yields_for_asset(asset_id) + pub async fn yields_for_asset(&self, asset_id: &AssetId) -> Result, GemstoneError> { + self.inner.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) } - pub async fn deposit(&self, provider: String, request: GemYieldDepositRequest) -> Result { - self.inner.deposit(&provider, &request).await.map_err(Into::into) + pub async fn deposit(&self, provider: String, asset: AssetId, wallet_address: String, amount: String) -> Result { + self.inner.deposit(&provider, &asset, &wallet_address, &amount).await.map_err(Into::into) } - pub async fn withdraw(&self, provider: String, request: GemYieldWithdrawRequest) -> Result { - self.inner.withdraw(&provider, &request).await.map_err(Into::into) + pub async fn withdraw(&self, provider: String, asset: AssetId, wallet_address: String, amount: String) -> Result { + self.inner.withdraw(&provider, &asset, &wallet_address, &amount).await.map_err(Into::into) } - pub async fn details(&self, provider: String, request: GemYieldDetailsRequest) -> Result { + pub async fn details(&self, provider: String, asset: AssetId, wallet_address: String) -> Result { + let request = YieldDetailsRequest { asset, wallet_address }; self.inner.details(&provider, &request).await.map_err(Into::into) } } diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index 82de38bb0..d80586cb5 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,12 +1,5 @@ use primitives::AssetId; -use yielder::{ - Yield as CoreYield, - YieldDepositRequest as CoreDepositRequest, - YieldDetails as CoreDetails, - YieldDetailsRequest as CoreDetailsRequest, - YieldTransaction as CoreTransaction, - YieldWithdrawRequest as CoreWithdrawRequest, -}; +use yielder::{Yield as CoreYield, YieldDetails as CoreDetails, YieldTransaction as CoreTransaction}; pub type GemYield = CoreYield; @@ -29,38 +22,6 @@ pub struct GemYieldTransaction { pub value: Option, } -pub type GemYieldDepositRequest = CoreDepositRequest; - -#[uniffi::remote(Record)] -pub struct GemYieldDepositRequest { - pub asset: AssetId, - pub wallet_address: String, - pub receiver_address: Option, - pub amount: String, - pub min_shares: Option, - pub partner_id: Option, -} - -pub type GemYieldWithdrawRequest = CoreWithdrawRequest; - -#[uniffi::remote(Record)] -pub struct GemYieldWithdrawRequest { - pub asset: AssetId, - pub wallet_address: String, - pub receiver_address: Option, - pub shares: String, - pub min_assets: Option, - pub partner_id: Option, -} - -pub type GemYieldDetailsRequest = CoreDetailsRequest; - -#[uniffi::remote(Record)] -pub struct GemYieldDetailsRequest { - pub asset: AssetId, - pub wallet_address: String, -} - pub type GemYieldDetails = CoreDetails; #[uniffi::remote(Record)] @@ -71,5 +32,6 @@ pub struct GemYieldDetails { pub asset_token: String, pub share_balance: Option, pub asset_balance: Option, + pub apy: Option, pub rewards: Option, } From fc4c7981c5f1bb05024ecab775dc88a5cad01606 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:43:27 +0900 Subject: [PATCH 03/33] code improvement --- Cargo.lock | 1 + crates/yielder/Cargo.toml | 1 + crates/yielder/src/lib.rs | 6 +- crates/yielder/src/provider.rs | 116 +++++++++++------- crates/yielder/src/yield_integration_tests.rs | 10 +- crates/yielder/src/yo/client.rs | 25 ++-- crates/yielder/src/yo/mod.rs | 4 +- crates/yielder/src/yo/provider.rs | 44 +++---- crates/yielder/src/yo/vault.rs | 10 +- gemstone/src/gem_yielder/mod.rs | 34 ++--- gemstone/src/gem_yielder/remote_types.rs | 29 +++-- 11 files changed, 153 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a00321677..52b3c5f12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9056,6 +9056,7 @@ dependencies = [ "gem_client", "gem_evm", "gem_jsonrpc", + "num-traits", "primitives", "reqwest", "serde_json", diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index 579e1eb64..16d4bae99 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -19,6 +19,7 @@ gem_client = { path = "../gem_client" } gem_evm = { path = "../gem_evm", features = ["rpc"] } primitives = { path = "../primitives" } async-trait = { workspace = true } +num-traits = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["macros"] } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 0f90082bb..64d208ff5 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -1,10 +1,8 @@ mod provider; pub mod yo; -pub use provider::{Yield, YieldDetails, YieldDetailsRequest, YieldProvider, YieldTransaction, Yielder}; -pub use yo::{ - IYoGateway, YO_ETH, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YO_USD, YieldError, YoGatewayApi, YoGatewayClient, YoVault, YoYieldProvider, vaults, -}; +pub use provider::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldProviderClient, YieldTransaction, Yielder}; +pub use yo::{IYoGateway, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YO_USD, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults}; #[cfg(all(test, feature = "yield_integration_tests"))] mod yield_integration_tests; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index b7ff7d220..a23ccf563 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{fmt, str::FromStr, sync::Arc}; use alloy_primitives::Address; use async_trait::async_trait; @@ -6,20 +6,50 @@ use primitives::{AssetId, Chain}; use crate::yo::YieldError; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum YieldProvider { + Yo, +} + +impl YieldProvider { + pub fn name(&self) -> &'static str { + match self { + YieldProvider::Yo => "yo", + } + } +} + +impl fmt::Display for YieldProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for YieldProvider { + type Err = YieldError; + + fn from_str(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + "yo" => Ok(YieldProvider::Yo), + other => Err(YieldError::new(format!("unknown yield provider {other}"))), + } + } +} + #[derive(Debug, Clone)] pub struct Yield { pub name: String, - pub asset: AssetId, - pub provider: String, + pub asset_id: AssetId, + pub provider: YieldProvider, pub apy: Option, } impl Yield { - pub fn new(name: impl Into, asset: AssetId, provider: impl Into, apy: Option) -> Self { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option) -> Self { Self { name: name.into(), - asset, - provider: provider.into(), + asset_id, + provider, apy, } } @@ -36,31 +66,31 @@ pub struct YieldTransaction { #[derive(Debug, Clone)] pub struct YieldDetailsRequest { - pub asset: AssetId, + pub asset_id: AssetId, pub wallet_address: String, } #[derive(Debug, Clone)] -pub struct YieldDetails { - pub asset: AssetId, - pub provider: String, - pub share_token: String, - pub asset_token: String, - pub share_balance: Option, - pub asset_balance: Option, +pub struct YieldPosition { + pub asset_id: AssetId, + pub provider: YieldProvider, + pub vault_token_address: String, + pub asset_token_address: String, + pub vault_balance_value: Option, + pub asset_balance_value: Option, pub apy: Option, pub rewards: Option, } -impl YieldDetails { - pub fn new(asset: AssetId, provider: impl Into, share_token: Address, asset_token: Address) -> Self { +impl YieldPosition { + pub fn new(asset_id: AssetId, provider: YieldProvider, share_token: Address, asset_token: Address) -> Self { Self { - asset, - provider: provider.into(), - share_token: share_token.to_string(), - asset_token: asset_token.to_string(), - share_balance: None, - asset_balance: None, + asset_id, + provider, + vault_token_address: share_token.to_string(), + asset_token_address: asset_token.to_string(), + vault_balance_value: None, + asset_balance_value: None, apy: None, rewards: None, } @@ -68,12 +98,12 @@ impl YieldDetails { } #[async_trait] -pub trait YieldProvider: Send + Sync { - fn protocol(&self) -> &'static str; +pub trait YieldProviderClient: Send + Sync { + fn provider(&self) -> YieldProvider; fn yields(&self, asset_id: &AssetId) -> Vec; - async fn deposit(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result; - async fn withdraw(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result; - async fn details(&self, request: &YieldDetailsRequest) -> Result; + async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result; + async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result; + async fn positions(&self, request: &YieldDetailsRequest) -> Result; async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { Ok(self.yields(asset_id)) } @@ -81,7 +111,7 @@ pub trait YieldProvider: Send + Sync { #[derive(Default)] pub struct Yielder { - providers: Vec>, + providers: Vec>, } impl Yielder { @@ -89,18 +119,18 @@ impl Yielder { Self { providers: Vec::new() } } - pub fn with_providers(providers: Vec>) -> Self { + pub fn with_providers(providers: Vec>) -> Self { Self { providers } } pub fn add_provider

(&mut self, provider: P) where - P: YieldProvider + 'static, + P: YieldProviderClient + 'static, { self.providers.push(Arc::new(provider)); } - pub fn add_provider_arc(&mut self, provider: Arc) { + pub fn add_provider_arc(&mut self, provider: Arc) { self.providers.push(provider); } @@ -117,26 +147,26 @@ impl Yielder { Ok(yields) } - pub async fn deposit(&self, protocol: &str, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { - let provider = self.provider(protocol)?; - provider.deposit(asset, wallet_address, amount).await + pub async fn deposit(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + let provider = self.provider(provider)?; + provider.deposit(asset_id, wallet_address, value).await } - pub async fn withdraw(&self, protocol: &str, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { - let provider = self.provider(protocol)?; - provider.withdraw(asset, wallet_address, amount).await + pub async fn withdraw(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + let provider = self.provider(provider)?; + provider.withdraw(asset_id, wallet_address, value).await } - pub async fn details(&self, protocol: &str, request: &YieldDetailsRequest) -> Result { - let provider = self.provider(protocol)?; - provider.details(request).await + pub async fn positions(&self, provider: YieldProvider, request: &YieldDetailsRequest) -> Result { + let provider = self.provider(provider)?; + provider.positions(request).await } - fn provider(&self, protocol: &str) -> Result, YieldError> { + fn provider(&self, provider: YieldProvider) -> Result, YieldError> { self.providers .iter() - .find(|provider| provider.protocol().eq_ignore_ascii_case(protocol)) + .find(|candidate| candidate.provider() == provider) .cloned() - .ok_or_else(|| YieldError::new(format!("provider {protocol} not found"))) + .ok_or_else(|| YieldError::new(format!("provider {provider} not found"))) } } diff --git a/crates/yielder/src/yield_integration_tests.rs b/crates/yielder/src/yield_integration_tests.rs index 608276a65..8f9cfe56a 100644 --- a/crates/yielder/src/yield_integration_tests.rs +++ b/crates/yielder/src/yield_integration_tests.rs @@ -6,7 +6,7 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use primitives::EVMChain; -use crate::{YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, Yielder, YoGatewayClient, YoYieldProvider}; +use crate::{YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoYieldProvider}; #[tokio::test] async fn yield_integration_test_fetches_performance_apy() -> Result<(), Box> { @@ -14,7 +14,7 @@ async fn yield_integration_test_fetches_performance_apy() -> Result<(), Box = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); + let provider: Arc = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); let yielder = Yielder::with_providers(vec![provider]); let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; @@ -24,10 +24,10 @@ async fn yield_integration_test_fetches_performance_apy() -> Result<(), Box -1.0, "apy should be > -100%"); let details = yielder - .details( - "yo", + .positions( + YieldProvider::Yo, &YieldDetailsRequest { - asset: YO_USD.asset_id(), + asset_id: YO_USD.asset_id(), wallet_address: "0x0000000000000000000000000000000000000000".to_string(), }, ) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 485e77d47..fb511a841 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -3,10 +3,11 @@ use alloy_sol_types::SolCall; use async_trait::async_trait; use gem_client::Client; use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; +use num_traits::ToPrimitive; use primitives::Chain; use serde_json::json; -use super::{YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YoVault, contract::IYoGateway, error::YieldError}; +use super::{YO_GATEWAY_BASE_MAINNET, YoVault, contract::IYoGateway, error::YieldError}; alloy_sol_types::sol! { interface IYoVaultToken { @@ -15,7 +16,7 @@ alloy_sol_types::sol! { } #[async_trait] -pub trait YoGatewayApi: Send + Sync { +pub trait YoProvider: Send + Sync { fn contract_address(&self) -> Address; fn chain(&self) -> Chain; fn build_deposit_transaction( @@ -49,10 +50,6 @@ pub struct YoGatewayClient { } impl YoGatewayClient { - pub const fn default_partner_id() -> u32 { - YO_PARTNER_ID_GEM - } - pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { Self { ethereum_client, @@ -220,7 +217,7 @@ impl YoGatewayClient { } #[async_trait] -impl YoGatewayApi for YoGatewayClient +impl YoProvider for YoGatewayClient where C: Client + Clone + Send + Sync + 'static, { @@ -297,21 +294,15 @@ where } async fn block_timestamp(&self, block_number: u64) -> Result { - let block_hex = format!("0x{block_number:x}"); - let mut blocks = self + let block = self .ethereum_client - .get_blocks(&[block_hex], false) + .get_block(block_number) .await .map_err(|err| YieldError::new(format!("yo gateway failed to fetch block {block_number}: {err}")))?; - let block = blocks - .pop() - .ok_or_else(|| YieldError::new(format!("yo gateway missing block data for {block_number}")))?; - block .timestamp - .to_string() - .parse::() - .map_err(|err| YieldError::new(format!("yo gateway failed to parse timestamp for block {block_number}: {err}"))) + .to_u64() + .ok_or_else(|| YieldError::new(format!("yo gateway failed to parse timestamp for block {block_number}"))) } } diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index a11eac4a3..7173e966f 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -4,11 +4,11 @@ mod error; mod provider; mod vault; -pub use client::{YoGatewayApi, YoGatewayClient}; +pub use client::{YoGatewayClient, YoProvider}; pub use contract::IYoGateway; pub use error::YieldError; pub use provider::YoYieldProvider; -pub use vault::{YO_ETH, YO_USD, YoVault, vaults}; +pub use vault::{YO_USD, YoVault, vaults}; use alloy_primitives::{Address, address}; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 8e2a3b8bf..4a7cbb969 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -6,9 +6,9 @@ use gem_evm::jsonrpc::TransactionObject; use primitives::AssetId; use tokio::try_join; -use crate::provider::{Yield, YieldDetails, YieldDetailsRequest, YieldProvider, YieldTransaction}; +use crate::provider::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldProviderClient, YieldTransaction}; -use super::{YO_PARTNER_ID_GEM, YoVault, client::YoGatewayApi, error::YieldError, vaults}; +use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; const SECONDS_PER_YEAR: f64 = 31_536_000.0; const APY_LOOKBACK_SECONDS: u64 = 7 * 24 * 60 * 60; @@ -16,11 +16,11 @@ const APY_LOOKBACK_SECONDS: u64 = 7 * 24 * 60 * 60; #[derive(Clone)] pub struct YoYieldProvider { vaults: Vec, - gateway: Arc, + gateway: Arc, } impl YoYieldProvider { - pub fn new(gateway: Arc) -> Self { + pub fn new(gateway: Arc) -> Self { Self { vaults: vaults().to_vec(), gateway, @@ -77,9 +77,9 @@ impl YoYieldProvider { } #[async_trait] -impl YieldProvider for YoYieldProvider { - fn protocol(&self) -> &'static str { - "yo" +impl YieldProviderClient for YoYieldProvider { + fn provider(&self) -> YieldProvider { + YieldProvider::Yo } fn yields(&self, asset_id: &AssetId) -> Vec { @@ -88,7 +88,7 @@ impl YieldProvider for YoYieldProvider { .filter_map(|vault| { let vault_asset = vault.asset_id(); if &vault_asset == asset_id { - Some(Yield::new(vault.name, vault_asset, self.protocol(), None)) + Some(Yield::new(vault.name, vault_asset, self.provider(), None)) } else { None } @@ -101,17 +101,17 @@ impl YieldProvider for YoYieldProvider { for vault in self.vaults.iter().copied().filter(|vault| vault.asset_id() == *asset_id) { let apy = self.performance_apy(vault).await?; - results.push(Yield::new(vault.name, vault.asset_id(), self.protocol(), apy)); + results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); } Ok(results) } - async fn deposit(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { - let vault = self.find_vault(asset)?; + async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + let vault = self.find_vault(asset_id)?; let wallet = parse_address(wallet_address)?; let receiver = wallet; - let amount = parse_amount(amount)?; + let amount = parse_value(value)?; let min_shares = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; @@ -121,11 +121,11 @@ impl YieldProvider for YoYieldProvider { Ok(convert_transaction(vault, tx)) } - async fn withdraw(&self, asset: &AssetId, wallet_address: &str, amount: &str) -> Result { - let vault = self.find_vault(asset)?; + async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + let vault = self.find_vault(asset_id)?; let wallet = parse_address(wallet_address)?; let receiver = wallet; - let shares = parse_amount(amount)?; + let shares = parse_value(value)?; let min_assets = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; @@ -135,16 +135,16 @@ impl YieldProvider for YoYieldProvider { Ok(convert_transaction(vault, tx)) } - async fn details(&self, request: &YieldDetailsRequest) -> Result { - let vault = self.find_vault(&request.asset)?; + async fn positions(&self, request: &YieldDetailsRequest) -> Result { + let vault = self.find_vault(&request.asset_id)?; let owner = parse_address(&request.wallet_address)?; - let mut details = YieldDetails::new(request.asset.clone(), self.protocol(), vault.yo_token, vault.asset_token); + let mut details = YieldPosition::new(request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); let share_balance = self.gateway.balance_of(vault.yo_token, owner).await?; - details.share_balance = Some(share_balance.to_string()); + details.vault_balance_value = Some(share_balance.to_string()); let asset_balance = self.gateway.balance_of(vault.asset_token, owner).await?; - details.asset_balance = Some(asset_balance.to_string()); + details.asset_balance_value = Some(asset_balance.to_string()); details.apy = self.performance_apy(vault).await?; @@ -156,8 +156,8 @@ fn parse_address(value: &str) -> Result { Address::from_str(value).map_err(|err| YieldError::new(format!("invalid address {value}: {err}"))) } -fn parse_amount(value: &str) -> Result { - U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid amount {value}: {err}"))) +fn parse_value(value: &str) -> Result { + U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid value {value}: {err}"))) } fn convert_transaction(vault: YoVault, tx: TransactionObject) -> YieldTransaction { diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index cb327b5ea..a846a9e46 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -34,14 +34,6 @@ pub const YO_USD: YoVault = YoVault::new( 6, ); -pub const YO_ETH: YoVault = YoVault::new( - "yoETH", - Chain::Base, - address!("0x3a43aec53490cb9fa922847385d82fe25d0e9de7"), - address!("0x4200000000000000000000000000000000000006"), - 18, -); - pub fn vaults() -> &'static [YoVault] { - &[YO_USD, YO_ETH] + &[YO_USD] } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 023dd4e0f..64452dd20 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -11,11 +11,11 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{YO_GATEWAY_BASE_MAINNET, YieldDetailsRequest, YieldProvider, Yielder, YoGatewayApi, YoGatewayClient, YoYieldProvider}; +use yielder::{YO_GATEWAY_BASE_MAINNET, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { - inner: Yielder, + yielder: Yielder, } impl std::fmt::Debug for GemYielder { @@ -31,28 +31,34 @@ impl GemYielder { let mut inner = Yielder::new(); let yo_provider = build_yo_provider(rpc_provider)?; inner.add_provider_arc(yo_provider); - Ok(Self { inner }) + Ok(Self { yielder: inner }) } pub async fn yields_for_asset(&self, asset_id: &AssetId) -> Result, GemstoneError> { - self.inner.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) + self.yielder.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) } - pub async fn deposit(&self, provider: String, asset: AssetId, wallet_address: String, amount: String) -> Result { - self.inner.deposit(&provider, &asset, &wallet_address, &amount).await.map_err(Into::into) + pub async fn deposit(&self, provider: String, asset: AssetId, wallet_address: String, value: String) -> Result { + let provider = provider.parse::()?; + self.yielder.deposit(provider, &asset, &wallet_address, &value).await.map_err(Into::into) } - pub async fn withdraw(&self, provider: String, asset: AssetId, wallet_address: String, amount: String) -> Result { - self.inner.withdraw(&provider, &asset, &wallet_address, &amount).await.map_err(Into::into) + pub async fn withdraw(&self, provider: String, asset: AssetId, wallet_address: String, value: String) -> Result { + let provider = provider.parse::()?; + self.yielder.withdraw(provider, &asset, &wallet_address, &value).await.map_err(Into::into) } - pub async fn details(&self, provider: String, asset: AssetId, wallet_address: String) -> Result { - let request = YieldDetailsRequest { asset, wallet_address }; - self.inner.details(&provider, &request).await.map_err(Into::into) + pub async fn positions(&self, provider: String, asset: AssetId, wallet_address: String) -> Result { + let provider = provider.parse::()?; + let request = YieldDetailsRequest { + asset_id: asset, + wallet_address, + }; + self.yielder.positions(provider, &request).await.map_err(Into::into) } } -fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { +fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { let endpoint = rpc_provider.get_endpoint(Chain::Base)?; let wrapper = AlienProviderWrapper { provider: rpc_provider }; let rpc_client = RpcClient::new(endpoint, Arc::new(wrapper)); @@ -60,7 +66,7 @@ fn build_yo_provider(rpc_provider: Arc) -> Result = Arc::new(gateway_client); - let provider: Arc = Arc::new(YoYieldProvider::new(gateway)); + let gateway: Arc = Arc::new(gateway_client); + let provider: Arc = Arc::new(YoYieldProvider::new(gateway)); Ok(provider) } diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index d80586cb5..c5434bbbd 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,13 +1,20 @@ use primitives::AssetId; -use yielder::{Yield as CoreYield, YieldDetails as CoreDetails, YieldTransaction as CoreTransaction}; +use yielder::{Yield as CoreYield, YieldPosition as CorePosition, YieldProvider as CoreYieldProvider, YieldTransaction as CoreTransaction}; + +pub type GemYieldProvider = CoreYieldProvider; + +#[uniffi::remote(Enum)] +pub enum GemYieldProvider { + Yo, +} pub type GemYield = CoreYield; #[uniffi::remote(Record)] pub struct GemYield { pub name: String, - pub asset: AssetId, - pub provider: String, + pub asset_id: AssetId, + pub provider: GemYieldProvider, pub apy: Option, } @@ -22,16 +29,16 @@ pub struct GemYieldTransaction { pub value: Option, } -pub type GemYieldDetails = CoreDetails; +pub type GemYieldPosition = CorePosition; #[uniffi::remote(Record)] -pub struct GemYieldDetails { - pub asset: AssetId, - pub provider: String, - pub share_token: String, - pub asset_token: String, - pub share_balance: Option, - pub asset_balance: Option, +pub struct GemYieldPosition { + pub asset_id: AssetId, + pub provider: GemYieldProvider, + pub vault_token_address: String, + pub asset_token_address: String, + pub vault_balance_value: Option, + pub asset_balance_value: Option, pub apy: Option, pub rewards: Option, } From fce8d43267724fc66ef3e92e3e6bf05b029ad29f Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:36:57 +0900 Subject: [PATCH 04/33] Refactor multicall3 usage and add YoVault position batching Refactored multicall3 to provide a builder-based batch interface with typed result decoding, replacing manual call construction and decoding in everstake and yielder modules. Added efficient position data fetching in YoGatewayClient using multicall batching for balances and historical prices, and updated YoYieldProvider to use this for APY and position queries. Added integration test for YoVault positions. Improved error handling and re-exported PositionData. --- crates/gem_evm/src/everstake/client.rs | 66 ++++---- crates/gem_evm/src/multicall3.rs | 186 +++++++++++++++-------- crates/gem_evm/src/rpc/client.rs | 52 ++++--- crates/yielder/src/yo/client.rs | 50 +++++- crates/yielder/src/yo/error.rs | 6 + crates/yielder/src/yo/mod.rs | 2 +- crates/yielder/src/yo/provider.rs | 59 ++----- crates/yielder/tests/integration_test.rs | 70 +++++++++ 8 files changed, 318 insertions(+), 173 deletions(-) create mode 100644 crates/yielder/tests/integration_test.rs diff --git a/crates/gem_evm/src/everstake/client.rs b/crates/gem_evm/src/everstake/client.rs index b23fd19d6..24d69bd79 100644 --- a/crates/gem_evm/src/everstake/client.rs +++ b/crates/gem_evm/src/everstake/client.rs @@ -3,7 +3,6 @@ pub const EVERSTAKE_STATS_PATH: &str = "/api/v1/stats"; pub const EVERSTAKE_VALIDATORS_QUEUE_PATH: &str = "/api/v1/validators/queue"; use super::{EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting, models::AccountState}; -use crate::multicall3::{IMulticall3, create_call3, decode_call3_return}; use alloy_primitives::Address; use gem_client::Client; @@ -34,26 +33,34 @@ pub async fn get_everstake_staking_apy() -> Result, Box(client: &EthereumClient, address: &str) -> Result> { let account = Address::from_str(address).map_err(|e| Box::new(e) as Box)?; let staker = account; - - let calls = vec![ - create_call3(EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting::depositedBalanceOfCall { account }), - create_call3(EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting::pendingBalanceOfCall { account }), - create_call3(EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting::pendingDepositedBalanceOfCall { account }), - create_call3(EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting::withdrawRequestCall { staker }), - create_call3(EVERSTAKE_ACCOUNTING_ADDRESS, IAccounting::restakedRewardOfCall { account }), - ]; - - let call_count = calls.len(); - let multicall_results = client.multicall3(calls).await?; - if multicall_results.len() != call_count { - return Err("Unexpected number of multicall results".into()); - } - - let deposited_balance = decode_balance_result::(&multicall_results[0]); - let pending_balance = decode_balance_result::(&multicall_results[1]); - let pending_deposited_balance = decode_balance_result::(&multicall_results[2]); - let withdraw_request = decode_call3_return::(&multicall_results[3])?; - let restaked_reward = decode_balance_result::(&multicall_results[4]); + let accounting: Address = EVERSTAKE_ACCOUNTING_ADDRESS.parse().unwrap(); + + let mut batch = client.multicall(); + let h_deposited = batch.add(accounting, IAccounting::depositedBalanceOfCall { account }); + let h_pending = batch.add(accounting, IAccounting::pendingBalanceOfCall { account }); + let h_pending_deposited = batch.add(accounting, IAccounting::pendingDepositedBalanceOfCall { account }); + let h_withdraw = batch.add(accounting, IAccounting::withdrawRequestCall { staker }); + let h_restaked = batch.add(accounting, IAccounting::restakedRewardOfCall { account }); + + let results = batch.execute().await.map_err(|e| e.to_string())?; + + let deposited_balance = results + .decode::(&h_deposited) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let pending_balance = results + .decode::(&h_pending) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let pending_deposited_balance = results + .decode::(&h_pending_deposited) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let withdraw_request = results.decode::(&h_withdraw)?; + let restaked_reward = results + .decode::(&h_restaked) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); Ok(AccountState { deposited_balance, @@ -64,21 +71,8 @@ pub async fn get_everstake_account_state(client: &EthereumCli }) } -fn decode_balance_result(result: &IMulticall3::Result) -> BigUint -where - T::Return: Into, -{ - if result.success { - decode_call3_return::(result) - .map(|value| { - let value: alloy_primitives::U256 = value.into(); - let bytes = value.to_be_bytes::<32>(); - BigUint::from_bytes_be(&bytes) - }) - .unwrap_or(BigUint::zero()) - } else { - BigUint::zero() - } +fn u256_to_biguint(value: alloy_primitives::U256) -> BigUint { + BigUint::from_bytes_be(&value.to_be_bytes::<32>()) } #[cfg(all(test, feature = "rpc", feature = "reqwest", feature = "chain_integration_tests"))] diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 89e4a9a8e..913ab1b63 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -1,52 +1,146 @@ +use std::{fmt, marker::PhantomData}; + +use alloy_primitives::{Address, hex}; use alloy_sol_types::{SolCall, sol}; -use primitives::EVMChain; +use gem_client::Client; +use primitives::chain_config::ChainStack; +use serde_json::json; + +use crate::rpc::EthereumClient; -// https://www.multicall3.com/ sol! { #[derive(Debug)] interface IMulticall3 { - struct Call { - address target; - bytes callData; - } - struct Call3 { address target; bool allowFailure; bytes callData; } - struct Call3Value { - address target; - bool allowFailure; - uint256 value; - bytes callData; - } - struct Result { bool success; bytes returnData; } - function aggregate(Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes[] memory returnData); - function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); + function getCurrentBlockTimestamp() external view returns (uint256 timestamp); + } +} + +/// Handle returned when adding a call to the batch. Used to decode the result. +pub struct CallHandle { + index: usize, + _marker: PhantomData, +} + +/// Results from executing a multicall batch +pub struct Multicall3Results { + results: Vec, +} + +impl Multicall3Results { + /// Decode the result for a specific call handle + pub fn decode(&self, handle: &CallHandle) -> Result { + let result = self + .results + .get(handle.index) + .ok_or_else(|| Multicall3Error(format!("invalid index: {}", handle.index)))?; + + if !result.success { + return Err(Multicall3Error(format!("{} failed", T::SIGNATURE))); + } + + T::abi_decode_returns(&result.returnData).map_err(|e| Multicall3Error(format!("{}: {:?}", T::SIGNATURE, e))) + } +} + +/// Builder for constructing multicall3 batches +pub struct Multicall3Builder<'a, C: Client + Clone> { + client: &'a EthereumClient, + calls: Vec, + block: Option, +} - function aggregate3Value(Call3Value[] calldata calls) - external - payable - returns (Result[] memory returnData); +impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { + pub fn new(client: &'a EthereumClient) -> Self { + Self { + client, + calls: Vec::new(), + block: None, + } + } - function tryAggregate(bool requireSuccess, Call[] calldata calls) - external - payable - returns (Result[] memory returnData); + /// Add a contract call to the batch + pub fn add(&mut self, target: Address, call: T) -> CallHandle { + let index = self.calls.len(); + self.calls.push(IMulticall3::Call3 { + target, + allowFailure: true, + callData: call.abi_encode().into(), + }); + CallHandle { index, _marker: PhantomData } + } + + /// Set the block number to execute at (default: latest) + pub fn at_block(mut self, block: u64) -> Self { + self.block = Some(block); + self + } + + /// Execute all calls in a single RPC request + pub async fn execute(self) -> Result { + if self.calls.is_empty() { + return Ok(Multicall3Results { results: vec![] }); + } + + let multicall_address = deployment_by_chain_stack(self.client.chain.chain_stack()); + let multicall_data = IMulticall3::aggregate3Call { calls: self.calls }.abi_encode(); + + let block_param = self + .block + .map(|n| serde_json::Value::String(format!("0x{n:x}"))) + .unwrap_or_else(|| json!("latest")); + + let result: String = self + .client + .client + .call( + "eth_call", + json!([{ + "to": multicall_address, + "data": hex::encode_prefixed(&multicall_data) + }, block_param]), + ) + .await + .map_err(|e| Multicall3Error(e.to_string()))?; + + let result_data = hex::decode(&result).map_err(|e| Multicall3Error(e.to_string()))?; + + let results = IMulticall3::aggregate3Call::abi_decode_returns(&result_data).map_err(|e| Multicall3Error(e.to_string()))?; + + Ok(Multicall3Results { results }) + } +} + +#[derive(Debug)] +pub struct Multicall3Error(pub String); + +impl fmt::Display for Multicall3Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for Multicall3Error {} + +pub fn deployment_by_chain_stack(stack: ChainStack) -> &'static str { + match stack { + ChainStack::ZkSync => "0xF9cda624FBC7e059355ce98a31693d299FACd963", + _ => "0xcA11bde05977b3631167028862bE2a173976CA11", } } +// Helpers for direct Call3 creation (used by swapper crate) pub fn create_call3(target: &str, call: impl SolCall) -> IMulticall3::Call3 { IMulticall3::Call3 { target: target.parse().unwrap(), @@ -55,42 +149,10 @@ pub fn create_call3(target: &str, call: impl SolCall) -> IMulticall3::Call3 { } } -pub fn decode_call3_return(result: &IMulticall3::Result) -> Result> { +pub fn decode_call3_return(result: &IMulticall3::Result) -> Result { if result.success { - let decoded = T::abi_decode_returns(&result.returnData).map_err(|e| format!("{:?} abi decode error: {:?}", T::SIGNATURE, e))?; - Ok(decoded) + T::abi_decode_returns(&result.returnData).map_err(|e| format!("{}: {:?}", T::SIGNATURE, e)) } else { - Err(format!("{:?} failed", T::SIGNATURE).into()) - } -} - -pub fn deployment_by_chain(chain: &EVMChain) -> &'static str { - match chain { - EVMChain::Ethereum - | EVMChain::Base - | EVMChain::Optimism - | EVMChain::Arbitrum - | EVMChain::AvalancheC - | EVMChain::Fantom - | EVMChain::SmartChain - | EVMChain::Polygon - | EVMChain::OpBNB - | EVMChain::Gnosis - | EVMChain::Manta - | EVMChain::Blast - | EVMChain::Linea - | EVMChain::Mantle - | EVMChain::Celo - | EVMChain::World - | EVMChain::Sonic - | EVMChain::Berachain - | EVMChain::Ink - | EVMChain::Unichain - | EVMChain::Hyperliquid - | EVMChain::Monad - | EVMChain::XLayer - | EVMChain::Plasma - | EVMChain::Stable => "0xcA11bde05977b3631167028862bE2a173976CA11", - EVMChain::ZkSync | EVMChain::Abstract => "0xF9cda624FBC7e059355ce98a31693d299FACd963", + Err(format!("{} failed", T::SIGNATURE)) } } diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index bab3c9fc7..57872be1f 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -15,14 +15,6 @@ use super::{ model::{Block, BlockTransactionsIds, EthSyncingStatus, Transaction, TransactionReciept, TransactionReplayTrace}, }; use crate::models::fee::EthereumFeeHistory; -#[cfg(feature = "rpc")] -use crate::multicall3::{ - IMulticall3, - IMulticall3::{Call3, Result as MulticallResult}, - deployment_by_chain, -}; -#[cfg(feature = "rpc")] -use alloy_sol_types::SolCall; use primitives::{Chain, EVMChain, NodeType}; pub const FUNCTION_ERC20_NAME: &str = "0x06fdde03"; @@ -268,23 +260,33 @@ impl EthereumClient { } #[cfg(feature = "rpc")] - pub async fn multicall3(&self, calls: Vec) -> Result, Box> { - let multicall_address = deployment_by_chain(&self.chain); - let multicall_data = IMulticall3::aggregate3Call { calls }.abi_encode(); - - let call = ( - "eth_call".to_string(), - json!([{ - "to": multicall_address, - "data": hex::encode_prefixed(&multicall_data) - }, "latest"]), - ); - - let result: String = self.call(call.0, call.1).await?; - let result_data = hex::decode(&result)?; - let multicall_results = - IMulticall3::aggregate3Call::abi_decode_returns(&result_data).map_err(|e| Box::new(e) as Box)?; + pub fn multicall(&self) -> crate::multicall3::Multicall3Builder<'_, C> { + crate::multicall3::Multicall3Builder::new(self) + } - Ok(multicall_results) + #[cfg(feature = "rpc")] + pub async fn multicall3( + &self, + calls: Vec, + ) -> Result, Box> { + use alloy_sol_types::SolCall; + + let multicall_address = crate::multicall3::deployment_by_chain_stack(self.chain.chain_stack()); + let multicall_data = crate::multicall3::IMulticall3::aggregate3Call { calls }.abi_encode(); + + let result: String = self + .client + .call( + "eth_call", + json!([{ + "to": multicall_address, + "data": hex::encode_prefixed(&multicall_data) + }, "latest"]), + ) + .await?; + + let result_data = hex::decode(&result)?; + let results = crate::multicall3::IMulticall3::aggregate3Call::abi_decode_returns(&result_data)?; + Ok(results) } } diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index fb511a841..0ab063742 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -2,7 +2,7 @@ use alloy_primitives::{Address, U256, hex}; use alloy_sol_types::SolCall; use async_trait::async_trait; use gem_client::Client; -use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; +use gem_evm::{jsonrpc::TransactionObject, multicall3::IMulticall3, rpc::EthereumClient}; use num_traits::ToPrimitive; use primitives::Chain; use serde_json::json; @@ -13,6 +13,21 @@ alloy_sol_types::sol! { interface IYoVaultToken { function convertToAssets(uint256 shares) external view returns (uint256 assets); } + + interface IERC20 { + function balanceOf(address account) external view returns (uint256); + } +} + +/// Result from fetching position data via multicall +#[derive(Debug, Clone)] +pub struct PositionData { + pub share_balance: U256, + pub asset_balance: U256, + pub latest_price: U256, + pub latest_timestamp: u64, + pub lookback_price: U256, + pub lookback_timestamp: u64, } #[async_trait] @@ -41,6 +56,9 @@ pub trait YoProvider: Send + Sync { async fn convert_to_assets_at_block(&self, yo_vault: Address, shares: U256, block_number: u64) -> Result; async fn latest_block_number(&self) -> Result; async fn block_timestamp(&self, block_number: u64) -> Result; + + /// Fetch position data including balances and historical prices for APY calculation + async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; } #[derive(Debug, Clone)] @@ -305,4 +323,34 @@ where .to_u64() .ok_or_else(|| YieldError::new(format!("yo gateway failed to parse timestamp for block {block_number}"))) } + + async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { + let latest_block = self.latest_block_number().await?; + let lookback_block = latest_block.saturating_sub(lookback_blocks); + let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); + let multicall_addr: Address = gem_evm::multicall3::deployment_by_chain_stack(self.ethereum_client.chain.chain_stack()) + .parse() + .unwrap(); + + let mut latest_batch = self.ethereum_client.multicall(); + let share_bal = latest_batch.add(vault.yo_token, IERC20::balanceOfCall { account: owner }); + let asset_bal = latest_batch.add(vault.asset_token, IERC20::balanceOfCall { account: owner }); + let latest_price = latest_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); + let latest_ts = latest_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); + + let mut lookback_batch = self.ethereum_client.multicall(); + let lookback_price = lookback_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); + let lookback_ts = lookback_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); + + let (latest, lookback) = tokio::try_join!(latest_batch.at_block(latest_block).execute(), lookback_batch.at_block(lookback_block).execute())?; + + Ok(PositionData { + share_balance: latest.decode::(&share_bal)?, + asset_balance: latest.decode::(&asset_bal)?, + latest_price: latest.decode::(&latest_price)?, + latest_timestamp: latest.decode::(&latest_ts)?.to::(), + lookback_price: lookback.decode::(&lookback_price)?, + lookback_timestamp: lookback.decode::(&lookback_ts)?.to::(), + }) + } } diff --git a/crates/yielder/src/yo/error.rs b/crates/yielder/src/yo/error.rs index bea72f6c1..9d288a8e4 100644 --- a/crates/yielder/src/yo/error.rs +++ b/crates/yielder/src/yo/error.rs @@ -32,3 +32,9 @@ impl From for YieldError { YieldError::new(value) } } + +impl From for YieldError { + fn from(e: gem_evm::multicall3::Multicall3Error) -> Self { + YieldError::new(e.to_string()) + } +} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 7173e966f..892cff288 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -4,7 +4,7 @@ mod error; mod provider; mod vault; -pub use client::{YoGatewayClient, YoProvider}; +pub use client::{PositionData, YoGatewayClient, YoProvider}; pub use contract::IYoGateway; pub use error::YieldError; pub use provider::YoYieldProvider; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 4a7cbb969..77e935eda 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -4,14 +4,15 @@ use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; use primitives::AssetId; -use tokio::try_join; use crate::provider::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldProviderClient, YieldTransaction}; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; const SECONDS_PER_YEAR: f64 = 31_536_000.0; -const APY_LOOKBACK_SECONDS: u64 = 7 * 24 * 60 * 60; + +// Base chain has ~2 second block time, 7 days lookback +const LOOKBACK_BLOCKS: u64 = 7 * 24 * 60 * 60 / 2; #[derive(Clone)] pub struct YoYieldProvider { @@ -34,46 +35,6 @@ impl YoYieldProvider { .find(|vault| vault.asset_id() == *asset_id) .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) } - - async fn performance_apy(&self, vault: YoVault) -> Result, YieldError> { - let latest_block = self.gateway.latest_block_number().await?; - let latest_timestamp = self.gateway.block_timestamp(latest_block).await?; - let target_timestamp = latest_timestamp.saturating_sub(APY_LOOKBACK_SECONDS); - let lookback_block = self.find_block_before(target_timestamp, latest_block).await?; - let (latest_price, lookback_price) = try_join!(self.share_price_at_block(vault, latest_block), self.share_price_at_block(vault, lookback_block))?; - let lookback_timestamp = self.gateway.block_timestamp(lookback_block).await?; - let elapsed = latest_timestamp.saturating_sub(lookback_timestamp); - Ok(annualize_growth(latest_price, lookback_price, elapsed)) - } - - async fn share_price_at_block(&self, vault: YoVault, block_number: u64) -> Result { - let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); - self.gateway.convert_to_assets_at_block(vault.yo_token, one_share, block_number).await - } - - async fn find_block_before(&self, target_timestamp: u64, latest_block: u64) -> Result { - let mut low = 0; - let mut high = latest_block; - let mut candidate = latest_block; - - while low <= high { - let mid = (low + high) / 2; - let mid_timestamp = self.gateway.block_timestamp(mid).await?; - - if mid_timestamp > target_timestamp { - if mid == 0 { - candidate = 0; - break; - } - high = mid - 1; - } else { - candidate = mid; - low = mid + 1; - } - } - - Ok(candidate) - } } #[async_trait] @@ -100,7 +61,9 @@ impl YieldProviderClient for YoYieldProvider { let mut results = Vec::new(); for vault in self.vaults.iter().copied().filter(|vault| vault.asset_id() == *asset_id) { - let apy = self.performance_apy(vault).await?; + let data = self.gateway.fetch_position_data(vault, Address::ZERO, LOOKBACK_BLOCKS).await?; + let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); + let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); } @@ -140,13 +103,13 @@ impl YieldProviderClient for YoYieldProvider { let owner = parse_address(&request.wallet_address)?; let mut details = YieldPosition::new(request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); - let share_balance = self.gateway.balance_of(vault.yo_token, owner).await?; - details.vault_balance_value = Some(share_balance.to_string()); + let data = self.gateway.fetch_position_data(vault, owner, LOOKBACK_BLOCKS).await?; - let asset_balance = self.gateway.balance_of(vault.asset_token, owner).await?; - details.asset_balance_value = Some(asset_balance.to_string()); + details.vault_balance_value = Some(data.share_balance.to_string()); + details.asset_balance_value = Some(data.asset_balance.to_string()); - details.apy = self.performance_apy(vault).await?; + let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); + details.apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); Ok(details) } diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs new file mode 100644 index 000000000..e2a5c0c69 --- /dev/null +++ b/crates/yielder/tests/integration_test.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use alloy_primitives::U256; +use gem_client::ReqwestClient; +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::EVMChain; +use yielder::{YO_USD, YieldDetailsRequest, YieldProviderClient, YoGatewayClient, YoYieldProvider}; + +fn base_rpc_url() -> String { + std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://mainnet.base.org".to_string()) +} + +#[tokio::test] +async fn test_yo_positions() { + let http_client = ReqwestClient::new_test_client(base_rpc_url()); + let jsonrpc_client = JsonRpcClient::new(http_client); + let eth_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); + let gateway = Arc::new(YoGatewayClient::base_mainnet(eth_client.clone())); + let gateway_client = YoGatewayClient::base_mainnet(eth_client); + let provider = YoYieldProvider::new(gateway); + + let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; + let asset_id = YO_USD.asset_id(); + + let request = YieldDetailsRequest { + asset_id: asset_id.clone(), + wallet_address: wallet_address.to_string(), + }; + + let position = provider.positions(&request).await.expect("should fetch positions"); + + println!("Position for {wallet_address}:"); + println!(" Asset ID: {}", position.asset_id); + println!(" Provider: {:?}", position.provider); + println!(" Vault Token: {}", position.vault_token_address); + println!(" Asset Token: {}", position.asset_token_address); + println!(" Vault Balance (yoUSD shares): {:?}", position.vault_balance_value); + println!(" Asset Balance (USDC): {:?}", position.asset_balance_value); + println!(" APY: {:?}", position.apy); + + // Parse balances and calculate actual USD value + let mut total_usd = 0.0; + + if let Some(vault_balance) = &position.vault_balance_value { + let shares: u128 = vault_balance.parse().unwrap_or(0); + let shares_formatted = shares as f64 / 1_000_000.0; + + // Get actual USDC value of shares + let shares_u256 = U256::from(shares); + let assets = gateway_client + .quote_convert_to_assets(YO_USD.yo_token, shares_u256) + .await + .expect("should convert shares to assets"); + let assets_value: u128 = assets.to_string().parse().unwrap_or(0); + let assets_usd = assets_value as f64 / 1_000_000.0; + + println!("\n yoUSD shares: {:.6} = ${:.6} USDC", shares_formatted, assets_usd); + total_usd += assets_usd; + } + + if let Some(asset_balance) = &position.asset_balance_value { + let usdc: u128 = asset_balance.parse().unwrap_or(0); + let usdc_formatted = usdc as f64 / 1_000_000.0; + println!(" USDC balance: ${:.6}", usdc_formatted); + total_usd += usdc_formatted; + } + + println!("\n TOTAL USD: ${:.2}", total_usd); +} From 96472fac6c6f2f633b1807377088a2d0f06e543e Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:20:02 +0900 Subject: [PATCH 05/33] add name to yield postion --- crates/yielder/src/provider.rs | 4 +++- crates/yielder/src/yo/provider.rs | 2 +- gemstone/src/gem_yielder/remote_types.rs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index a23ccf563..9743dab7a 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -72,6 +72,7 @@ pub struct YieldDetailsRequest { #[derive(Debug, Clone)] pub struct YieldPosition { + pub name: String, pub asset_id: AssetId, pub provider: YieldProvider, pub vault_token_address: String, @@ -83,8 +84,9 @@ pub struct YieldPosition { } impl YieldPosition { - pub fn new(asset_id: AssetId, provider: YieldProvider, share_token: Address, asset_token: Address) -> Self { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, share_token: Address, asset_token: Address) -> Self { Self { + name: name.into(), asset_id, provider, vault_token_address: share_token.to_string(), diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 77e935eda..7784a4486 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -101,7 +101,7 @@ impl YieldProviderClient for YoYieldProvider { async fn positions(&self, request: &YieldDetailsRequest) -> Result { let vault = self.find_vault(&request.asset_id)?; let owner = parse_address(&request.wallet_address)?; - let mut details = YieldPosition::new(request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); + let mut details = YieldPosition::new(vault.name, request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); let data = self.gateway.fetch_position_data(vault, owner, LOOKBACK_BLOCKS).await?; diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index c5434bbbd..f9256dd09 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -33,6 +33,7 @@ pub type GemYieldPosition = CorePosition; #[uniffi::remote(Record)] pub struct GemYieldPosition { + pub name: String, pub asset_id: AssetId, pub provider: GemYieldProvider, pub vault_token_address: String, From c2780b75065dfd0ad24051672900c23aaddfb108 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:34:46 +0900 Subject: [PATCH 06/33] code cleanup --- Cargo.lock | 2 +- crates/gem_evm/src/call_decoder.rs | 1 + crates/gem_evm/src/contracts/erc20.rs | 1 + crates/gem_evm/src/lib.rs | 1 + crates/yielder/src/lib.rs | 7 +- crates/yielder/src/models.rs | 99 +++++++++++++++++++ crates/yielder/src/provider.rs | 99 +------------------ crates/yielder/src/yield_integration_tests.rs | 39 -------- crates/yielder/src/yo/client.rs | 38 ++----- crates/yielder/src/yo/contract.rs | 4 + crates/yielder/src/yo/error.rs | 6 +- crates/yielder/src/yo/mod.rs | 6 +- crates/yielder/src/yo/model.rs | 12 +++ crates/yielder/src/yo/provider.rs | 3 +- crates/yielder/tests/integration_test.rs | 37 ++++++- gemstone/src/gem_yielder/remote_types.rs | 10 +- gemstone/src/lib.rs | 5 +- 17 files changed, 185 insertions(+), 185 deletions(-) create mode 100644 crates/yielder/src/models.rs delete mode 100644 crates/yielder/src/yield_integration_tests.rs create mode 100644 crates/yielder/src/yo/model.rs diff --git a/Cargo.lock b/Cargo.lock index 5f3b21ef1..19e32dba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9110,7 +9110,7 @@ dependencies = [ "gem_jsonrpc", "num-traits", "primitives", - "reqwest", + "reqwest 0.13.1", "serde_json", "tokio", ] diff --git a/crates/gem_evm/src/call_decoder.rs b/crates/gem_evm/src/call_decoder.rs index 83b1d2958..5e721e4ec 100644 --- a/crates/gem_evm/src/call_decoder.rs +++ b/crates/gem_evm/src/call_decoder.rs @@ -111,6 +111,7 @@ impl From for DecodedCall { IERC20Calls::name(_) => ("name", vec![]), IERC20Calls::symbol(_) => ("symbol", vec![]), IERC20Calls::decimals(_) => ("decimals", vec![]), + IERC20Calls::balanceOf(balance_of) => ("balanceOf", vec![("account", "address", balance_of.account.to_string())]), IERC20Calls::allowance(allowance) => ( "allowance", vec![ diff --git a/crates/gem_evm/src/contracts/erc20.rs b/crates/gem_evm/src/contracts/erc20.rs index 130d7dc9f..e13752b22 100644 --- a/crates/gem_evm/src/contracts/erc20.rs +++ b/crates/gem_evm/src/contracts/erc20.rs @@ -7,6 +7,7 @@ sol! { function name() public view virtual returns (string memory); function symbol() public view virtual returns (string memory); function decimals() public view virtual returns (uint8); + function balanceOf(address account) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); function transfer(address to, uint256 value) external returns (bool); diff --git a/crates/gem_evm/src/lib.rs b/crates/gem_evm/src/lib.rs index b608e4fff..1aafa4af5 100644 --- a/crates/gem_evm/src/lib.rs +++ b/crates/gem_evm/src/lib.rs @@ -14,6 +14,7 @@ pub mod everstake; pub mod fee_calculator; pub mod jsonrpc; pub mod monad; +#[cfg(feature = "rpc")] pub mod multicall3; pub mod permit2; #[cfg(feature = "rpc")] diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 64d208ff5..4faa5af4a 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -1,8 +1,7 @@ +mod models; mod provider; pub mod yo; -pub use provider::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldProviderClient, YieldTransaction, Yielder}; +pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +pub use provider::{YieldProviderClient, Yielder}; pub use yo::{IYoGateway, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YO_USD, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults}; - -#[cfg(all(test, feature = "yield_integration_tests"))] -mod yield_integration_tests; diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs new file mode 100644 index 000000000..c1e75fcdd --- /dev/null +++ b/crates/yielder/src/models.rs @@ -0,0 +1,99 @@ +use std::{fmt, str::FromStr}; + +use alloy_primitives::Address; +use primitives::{AssetId, Chain}; + +use crate::yo::YieldError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum YieldProvider { + Yo, +} + +impl YieldProvider { + pub fn name(&self) -> &'static str { + match self { + YieldProvider::Yo => "yo", + } + } +} + +impl fmt::Display for YieldProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for YieldProvider { + type Err = YieldError; + + fn from_str(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + "yo" => Ok(YieldProvider::Yo), + other => Err(YieldError::new(format!("unknown yield provider {other}"))), + } + } +} + +#[derive(Debug, Clone)] +pub struct Yield { + pub name: String, + pub asset_id: AssetId, + pub provider: YieldProvider, + pub apy: Option, +} + +impl Yield { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option) -> Self { + Self { + name: name.into(), + asset_id, + provider, + apy, + } + } +} + +#[derive(Debug, Clone)] +pub struct YieldTransaction { + pub chain: Chain, + pub from: String, + pub to: String, + pub data: String, + pub value: Option, +} + +#[derive(Debug, Clone)] +pub struct YieldDetailsRequest { + pub asset_id: AssetId, + pub wallet_address: String, +} + +#[derive(Debug, Clone)] +pub struct YieldPosition { + pub name: String, + pub asset_id: AssetId, + pub provider: YieldProvider, + pub vault_token_address: String, + pub asset_token_address: String, + pub vault_balance_value: Option, + pub asset_balance_value: Option, + pub apy: Option, + pub rewards: Option, +} + +impl YieldPosition { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, share_token: Address, asset_token: Address) -> Self { + Self { + name: name.into(), + asset_id, + provider, + vault_token_address: share_token.to_string(), + asset_token_address: asset_token.to_string(), + vault_balance_value: None, + asset_balance_value: None, + apy: None, + rewards: None, + } + } +} diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 9743dab7a..02b9287b3 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -1,104 +1,11 @@ -use std::{fmt, str::FromStr, sync::Arc}; +use std::sync::Arc; -use alloy_primitives::Address; use async_trait::async_trait; -use primitives::{AssetId, Chain}; +use primitives::AssetId; +use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::yo::YieldError; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum YieldProvider { - Yo, -} - -impl YieldProvider { - pub fn name(&self) -> &'static str { - match self { - YieldProvider::Yo => "yo", - } - } -} - -impl fmt::Display for YieldProvider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.name()) - } -} - -impl FromStr for YieldProvider { - type Err = YieldError; - - fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - "yo" => Ok(YieldProvider::Yo), - other => Err(YieldError::new(format!("unknown yield provider {other}"))), - } - } -} - -#[derive(Debug, Clone)] -pub struct Yield { - pub name: String, - pub asset_id: AssetId, - pub provider: YieldProvider, - pub apy: Option, -} - -impl Yield { - pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option) -> Self { - Self { - name: name.into(), - asset_id, - provider, - apy, - } - } -} - -#[derive(Debug, Clone)] -pub struct YieldTransaction { - pub chain: Chain, - pub from: String, - pub to: String, - pub data: String, - pub value: Option, -} - -#[derive(Debug, Clone)] -pub struct YieldDetailsRequest { - pub asset_id: AssetId, - pub wallet_address: String, -} - -#[derive(Debug, Clone)] -pub struct YieldPosition { - pub name: String, - pub asset_id: AssetId, - pub provider: YieldProvider, - pub vault_token_address: String, - pub asset_token_address: String, - pub vault_balance_value: Option, - pub asset_balance_value: Option, - pub apy: Option, - pub rewards: Option, -} - -impl YieldPosition { - pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, share_token: Address, asset_token: Address) -> Self { - Self { - name: name.into(), - asset_id, - provider, - vault_token_address: share_token.to_string(), - asset_token_address: asset_token.to_string(), - vault_balance_value: None, - asset_balance_value: None, - apy: None, - rewards: None, - } - } -} - #[async_trait] pub trait YieldProviderClient: Send + Sync { fn provider(&self) -> YieldProvider; diff --git a/crates/yielder/src/yield_integration_tests.rs b/crates/yielder/src/yield_integration_tests.rs deleted file mode 100644 index 8f9cfe56a..000000000 --- a/crates/yielder/src/yield_integration_tests.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![cfg(all(test, feature = "yield_integration_tests"))] - -use std::sync::Arc; - -use gem_evm::rpc::EthereumClient; -use gem_jsonrpc::client::JsonRpcClient; -use primitives::EVMChain; - -use crate::{YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoYieldProvider}; - -#[tokio::test] -async fn yield_integration_test_fetches_performance_apy() -> Result<(), Box> { - let rpc_url = std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://mainnet.base.org".to_string()); - let jsonrpc_client = JsonRpcClient::new_reqwest(rpc_url); - let ethereum_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); - let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); - let provider: Arc = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); - let yielder = Yielder::with_providers(vec![provider]); - - let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; - assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); - let apy = apy_yields[0].apy.expect("apy should be computed"); - assert!(apy.is_finite(), "apy should be finite"); - assert!(apy > -1.0, "apy should be > -100%"); - - let details = yielder - .positions( - YieldProvider::Yo, - &YieldDetailsRequest { - asset_id: YO_USD.asset_id(), - wallet_address: "0x0000000000000000000000000000000000000000".to_string(), - }, - ) - .await?; - - assert!(details.apy.is_some(), "apy should be present in details"); - - Ok(()) -} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 0ab063742..a8b3b1b42 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -2,33 +2,17 @@ use alloy_primitives::{Address, U256, hex}; use alloy_sol_types::SolCall; use async_trait::async_trait; use gem_client::Client; -use gem_evm::{jsonrpc::TransactionObject, multicall3::IMulticall3, rpc::EthereumClient}; +use gem_evm::contracts::IERC20; +use gem_evm::multicall3::IMulticall3; +use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; use num_traits::ToPrimitive; use primitives::Chain; use serde_json::json; -use super::{YO_GATEWAY_BASE_MAINNET, YoVault, contract::IYoGateway, error::YieldError}; - -alloy_sol_types::sol! { - interface IYoVaultToken { - function convertToAssets(uint256 shares) external view returns (uint256 assets); - } - - interface IERC20 { - function balanceOf(address account) external view returns (uint256); - } -} - -/// Result from fetching position data via multicall -#[derive(Debug, Clone)] -pub struct PositionData { - pub share_balance: U256, - pub asset_balance: U256, - pub latest_price: U256, - pub latest_timestamp: u64, - pub lookback_price: U256, - pub lookback_timestamp: u64, -} +use super::contract::{IYoGateway, IYoVaultToken}; +use super::error::YieldError; +use super::model::PositionData; +use super::{YO_GATEWAY_BASE_MAINNET, YoVault}; #[async_trait] pub trait YoProvider: Send + Sync { @@ -272,13 +256,7 @@ where } async fn balance_of(&self, token: Address, owner: Address) -> Result { - alloy_sol_types::sol! { - interface IERC20Balance { - function balanceOf(address account) external view returns (uint256); - } - } - - let call = IERC20Balance::balanceOfCall { account: owner }.abi_encode(); + let call = IERC20::balanceOfCall { account: owner }.abi_encode(); let payload = hex::encode_prefixed(call); let params = json!([ { diff --git a/crates/yielder/src/yo/contract.rs b/crates/yielder/src/yo/contract.rs index 227393ce9..cb9c12ab9 100644 --- a/crates/yielder/src/yo/contract.rs +++ b/crates/yielder/src/yo/contract.rs @@ -1,6 +1,10 @@ use alloy_sol_types::sol; sol! { + interface IYoVaultToken { + function convertToAssets(uint256 shares) external view returns (uint256 assets); + } + interface IYoGateway { function quoteConvertToShares(address yoVault, uint256 assets) external view returns (uint256 shares); diff --git a/crates/yielder/src/yo/error.rs b/crates/yielder/src/yo/error.rs index 9d288a8e4..5ce0e008d 100644 --- a/crates/yielder/src/yo/error.rs +++ b/crates/yielder/src/yo/error.rs @@ -1,5 +1,7 @@ use std::{error::Error, fmt}; +use gem_evm::multicall3::Multicall3Error; + #[derive(Debug, Clone)] pub struct YieldError(String); @@ -33,8 +35,8 @@ impl From for YieldError { } } -impl From for YieldError { - fn from(e: gem_evm::multicall3::Multicall3Error) -> Self { +impl From for YieldError { + fn from(e: Multicall3Error) -> Self { YieldError::new(e.to_string()) } } diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 892cff288..94c5c0898 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -1,11 +1,13 @@ mod client; mod contract; mod error; +mod model; mod provider; mod vault; -pub use client::{PositionData, YoGatewayClient, YoProvider}; -pub use contract::IYoGateway; +pub use client::{YoGatewayClient, YoProvider}; +pub use contract::{IYoGateway, IYoVaultToken}; +pub use model::PositionData; pub use error::YieldError; pub use provider::YoYieldProvider; pub use vault::{YO_USD, YoVault, vaults}; diff --git a/crates/yielder/src/yo/model.rs b/crates/yielder/src/yo/model.rs new file mode 100644 index 000000000..219366748 --- /dev/null +++ b/crates/yielder/src/yo/model.rs @@ -0,0 +1,12 @@ +use alloy_primitives::U256; + +/// Result from fetching position data via multicall +#[derive(Debug, Clone)] +pub struct PositionData { + pub share_balance: U256, + pub asset_balance: U256, + pub latest_price: U256, + pub latest_timestamp: u64, + pub lookback_price: U256, + pub lookback_timestamp: u64, +} diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 7784a4486..e48664bbb 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -5,7 +5,8 @@ use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; use primitives::AssetId; -use crate::provider::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldProviderClient, YieldTransaction}; +use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +use crate::provider::YieldProviderClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index e2a5c0c69..5748fa3cd 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "yield_integration_tests")] + use std::sync::Arc; use alloy_primitives::U256; @@ -5,12 +7,43 @@ use gem_client::ReqwestClient; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use primitives::EVMChain; -use yielder::{YO_USD, YieldDetailsRequest, YieldProviderClient, YoGatewayClient, YoYieldProvider}; +use yielder::{ + YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoYieldProvider, +}; fn base_rpc_url() -> String { std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://mainnet.base.org".to_string()) } +#[tokio::test] +async fn test_yields_for_asset_with_apy() -> Result<(), Box> { + let jsonrpc_client = JsonRpcClient::new_reqwest(base_rpc_url()); + let ethereum_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); + let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); + let provider: Arc = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); + let yielder = Yielder::with_providers(vec![provider]); + + let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; + assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); + let apy = apy_yields[0].apy.expect("apy should be computed"); + assert!(apy.is_finite(), "apy should be finite"); + assert!(apy > -1.0, "apy should be > -100%"); + + let details = yielder + .positions( + YieldProvider::Yo, + &YieldDetailsRequest { + asset_id: YO_USD.asset_id(), + wallet_address: "0x0000000000000000000000000000000000000000".to_string(), + }, + ) + .await?; + + assert!(details.apy.is_some(), "apy should be present in details"); + + Ok(()) +} + #[tokio::test] async fn test_yo_positions() { let http_client = ReqwestClient::new_test_client(base_rpc_url()); @@ -39,14 +72,12 @@ async fn test_yo_positions() { println!(" Asset Balance (USDC): {:?}", position.asset_balance_value); println!(" APY: {:?}", position.apy); - // Parse balances and calculate actual USD value let mut total_usd = 0.0; if let Some(vault_balance) = &position.vault_balance_value { let shares: u128 = vault_balance.parse().unwrap_or(0); let shares_formatted = shares as f64 / 1_000_000.0; - // Get actual USDC value of shares let shares_u256 = U256::from(shares); let assets = gateway_client .quote_convert_to_assets(YO_USD.yo_token, shares_u256) diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index f9256dd09..385506932 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,14 +1,14 @@ use primitives::AssetId; -use yielder::{Yield as CoreYield, YieldPosition as CorePosition, YieldProvider as CoreYieldProvider, YieldTransaction as CoreTransaction}; +use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; -pub type GemYieldProvider = CoreYieldProvider; +pub type GemYieldProvider = YieldProvider; #[uniffi::remote(Enum)] pub enum GemYieldProvider { Yo, } -pub type GemYield = CoreYield; +pub type GemYield = Yield; #[uniffi::remote(Record)] pub struct GemYield { @@ -18,7 +18,7 @@ pub struct GemYield { pub apy: Option, } -pub type GemYieldTransaction = CoreTransaction; +pub type GemYieldTransaction = YieldTransaction; #[uniffi::remote(Record)] pub struct GemYieldTransaction { @@ -29,7 +29,7 @@ pub struct GemYieldTransaction { pub value: Option, } -pub type GemYieldPosition = CorePosition; +pub type GemYieldPosition = YieldPosition; #[uniffi::remote(Record)] pub struct GemYieldPosition { diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index e20d77414..f0e1a56eb 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -17,6 +17,7 @@ pub mod siwe; pub mod wallet_connect; use alien::AlienError; +use yielder::YieldError; uniffi::setup_scaffolding!("gemstone"); static LIB_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -107,8 +108,8 @@ impl From for GemstoneError { Self::AnyError { msg: error.to_string() } } } -impl From for GemstoneError { - fn from(error: yielder::yo::YieldError) -> Self { +impl From for GemstoneError { + fn from(error: YieldError) -> Self { Self::AnyError { msg: error.to_string() } } } From 5529436807b6ed0086173cd1b60bc469ab480406 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:52:57 +0900 Subject: [PATCH 07/33] more code cleanup --- crates/gem_evm/src/everstake/client.rs | 20 ++++++++++---------- crates/gem_evm/src/rpc/client.rs | 26 +++++++++++--------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/crates/gem_evm/src/everstake/client.rs b/crates/gem_evm/src/everstake/client.rs index 24d69bd79..c09b430b3 100644 --- a/crates/gem_evm/src/everstake/client.rs +++ b/crates/gem_evm/src/everstake/client.rs @@ -36,29 +36,29 @@ pub async fn get_everstake_account_state(client: &EthereumCli let accounting: Address = EVERSTAKE_ACCOUNTING_ADDRESS.parse().unwrap(); let mut batch = client.multicall(); - let h_deposited = batch.add(accounting, IAccounting::depositedBalanceOfCall { account }); - let h_pending = batch.add(accounting, IAccounting::pendingBalanceOfCall { account }); - let h_pending_deposited = batch.add(accounting, IAccounting::pendingDepositedBalanceOfCall { account }); - let h_withdraw = batch.add(accounting, IAccounting::withdrawRequestCall { staker }); - let h_restaked = batch.add(accounting, IAccounting::restakedRewardOfCall { account }); + let deposited = batch.add(accounting, IAccounting::depositedBalanceOfCall { account }); + let pending = batch.add(accounting, IAccounting::pendingBalanceOfCall { account }); + let pending_deposited = batch.add(accounting, IAccounting::pendingDepositedBalanceOfCall { account }); + let withdraw = batch.add(accounting, IAccounting::withdrawRequestCall { staker }); + let restaked = batch.add(accounting, IAccounting::restakedRewardOfCall { account }); let results = batch.execute().await.map_err(|e| e.to_string())?; let deposited_balance = results - .decode::(&h_deposited) + .decode::(&deposited) .map(u256_to_biguint) .unwrap_or_else(|_| BigUint::zero()); let pending_balance = results - .decode::(&h_pending) + .decode::(&pending) .map(u256_to_biguint) .unwrap_or_else(|_| BigUint::zero()); let pending_deposited_balance = results - .decode::(&h_pending_deposited) + .decode::(&pending_deposited) .map(u256_to_biguint) .unwrap_or_else(|_| BigUint::zero()); - let withdraw_request = results.decode::(&h_withdraw)?; + let withdraw_request = results.decode::(&withdraw)?; let restaked_reward = results - .decode::(&h_restaked) + .decode::(&restaked) .map(u256_to_biguint) .unwrap_or_else(|_| BigUint::zero()); diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index 57872be1f..cd0d25a6c 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -2,20 +2,19 @@ use alloy_primitives::{Address, Bytes, hex}; use gem_client::Client; use gem_jsonrpc::client::JsonRpcClient as GenericJsonRpcClient; use gem_jsonrpc::types::{ERROR_INTERNAL_ERROR, JsonRpcError, JsonRpcResult}; - use num_bigint::{BigInt, Sign}; +use primitives::{Chain, EVMChain, NodeType}; use serde::de::DeserializeOwned; use serde_json::json; use serde_serializers::biguint_from_hex_str; use std::any::TypeId; use std::str::FromStr; -use super::{ - ankr::AnkrClient, - model::{Block, BlockTransactionsIds, EthSyncingStatus, Transaction, TransactionReciept, TransactionReplayTrace}, -}; +use super::ankr::AnkrClient; +use super::model::{Block, BlockTransactionsIds, EthSyncingStatus, Transaction, TransactionReciept, TransactionReplayTrace}; use crate::models::fee::EthereumFeeHistory; -use primitives::{Chain, EVMChain, NodeType}; +#[cfg(feature = "rpc")] +use crate::multicall3::{IMulticall3, Multicall3Builder, deployment_by_chain_stack}; pub const FUNCTION_ERC20_NAME: &str = "0x06fdde03"; pub const FUNCTION_ERC20_SYMBOL: &str = "0x95d89b41"; @@ -260,19 +259,16 @@ impl EthereumClient { } #[cfg(feature = "rpc")] - pub fn multicall(&self) -> crate::multicall3::Multicall3Builder<'_, C> { - crate::multicall3::Multicall3Builder::new(self) + pub fn multicall(&self) -> Multicall3Builder<'_, C> { + Multicall3Builder::new(self) } #[cfg(feature = "rpc")] - pub async fn multicall3( - &self, - calls: Vec, - ) -> Result, Box> { + pub async fn multicall3(&self, calls: Vec) -> Result, Box> { use alloy_sol_types::SolCall; - let multicall_address = crate::multicall3::deployment_by_chain_stack(self.chain.chain_stack()); - let multicall_data = crate::multicall3::IMulticall3::aggregate3Call { calls }.abi_encode(); + let multicall_address = deployment_by_chain_stack(self.chain.chain_stack()); + let multicall_data = IMulticall3::aggregate3Call { calls }.abi_encode(); let result: String = self .client @@ -286,7 +282,7 @@ impl EthereumClient { .await?; let result_data = hex::decode(&result)?; - let results = crate::multicall3::IMulticall3::aggregate3Call::abi_decode_returns(&result_data)?; + let results = IMulticall3::aggregate3Call::abi_decode_returns(&result_data)?; Ok(results) } } From 8409d3ae38e94b3021b70034316a422d1c8fb601 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:24:57 +0900 Subject: [PATCH 08/33] Add yield availability check and fix asset value calc Introduces an is_yield_available method to Yielder and GemYielder for checking if yield is available for a given asset. Also corrects asset value calculation in YoYieldProvider to derive it from share balance and latest price. --- crates/yielder/src/provider.rs | 4 ++++ crates/yielder/src/yo/provider.rs | 6 +++++- gemstone/src/gem_yielder/mod.rs | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 02b9287b3..6f60a13bc 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -47,6 +47,10 @@ impl Yielder { self.providers.iter().flat_map(|provider| provider.yields(asset_id)).collect() } + pub fn is_yield_available(&self, asset_id: &AssetId) -> bool { + self.providers.iter().any(|provider| !provider.yields(asset_id).is_empty()) + } + pub async fn yields_for_asset_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut yields = Vec::new(); for provider in &self.providers { diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index e48664bbb..f6172c654 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -107,7 +107,11 @@ impl YieldProviderClient for YoYieldProvider { let data = self.gateway.fetch_position_data(vault, owner, LOOKBACK_BLOCKS).await?; details.vault_balance_value = Some(data.share_balance.to_string()); - details.asset_balance_value = Some(data.asset_balance.to_string()); + + // Calculate asset value from shares: share_balance * latest_price / one_share + let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); + let asset_value = data.share_balance.saturating_mul(data.latest_price) / one_share; + details.asset_balance_value = Some(asset_value.to_string()); let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); details.apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 64452dd20..c70f09157 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -38,6 +38,10 @@ impl GemYielder { self.yielder.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) } + pub fn is_yield_available(&self, asset_id: &AssetId) -> bool { + self.yielder.is_yield_available(asset_id) + } + pub async fn deposit(&self, provider: String, asset: AssetId, wallet_address: String, value: String) -> Result { let provider = provider.parse::()?; self.yielder.deposit(provider, &asset, &wallet_address, &value).await.map_err(Into::into) From 020d9249e17b79481fc46cafeb1dd3651244d600 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:43:33 +0900 Subject: [PATCH 09/33] Add multi-chain support for Yo yield provider Refactored YoYieldProvider to support multiple chains by managing gateways per chain and updating vault definitions. Added USDT vault for Ethereum, replaced YO_GATEWAY_BASE_MAINNET with YO_GATEWAY, and updated GemYielder to initialize gateways for both Base and Ethereum chains. Adjusted lookback block calculation and related logic to be chain-aware. --- crates/yielder/src/lib.rs | 4 +- crates/yielder/src/yo/client.rs | 212 ++---------------------------- crates/yielder/src/yo/mod.rs | 8 +- crates/yielder/src/yo/provider.rs | 45 ++++--- crates/yielder/src/yo/vault.rs | 10 +- gemstone/src/gem_yielder/mod.rs | 37 ++++-- 6 files changed, 83 insertions(+), 233 deletions(-) diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 4faa5af4a..e64a25512 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -4,4 +4,6 @@ pub mod yo; pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; -pub use yo::{IYoGateway, YO_GATEWAY_BASE_MAINNET, YO_PARTNER_ID_GEM, YO_USD, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults}; +pub use yo::{ + IYoGateway, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults, +}; diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index a8b3b1b42..85ffa0698 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -1,23 +1,19 @@ -use alloy_primitives::{Address, U256, hex}; +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::multicall3::IMulticall3; use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; -use num_traits::ToPrimitive; -use primitives::Chain; -use serde_json::json; use super::contract::{IYoGateway, IYoVaultToken}; use super::error::YieldError; use super::model::PositionData; -use super::{YO_GATEWAY_BASE_MAINNET, YoVault}; +use super::YoVault; #[async_trait] pub trait YoProvider: Send + Sync { fn contract_address(&self) -> Address; - fn chain(&self) -> Chain; fn build_deposit_transaction( &self, from: Address, @@ -36,12 +32,6 @@ pub trait YoProvider: Send + Sync { receiver: Address, partner_id: u32, ) -> TransactionObject; - async fn balance_of(&self, token: Address, owner: Address) -> Result; - async fn convert_to_assets_at_block(&self, yo_vault: Address, shares: U256, block_number: u64) -> Result; - async fn latest_block_number(&self) -> Result; - async fn block_timestamp(&self, block_number: u64) -> Result; - - /// Fetch position data including balances and historical prices for APY calculation async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; } @@ -59,67 +49,7 @@ impl YoGatewayClient { } } - pub fn base_mainnet(ethereum_client: EthereumClient) -> Self { - Self::new(ethereum_client, YO_GATEWAY_BASE_MAINNET) - } - - pub fn contract_address(&self) -> Address { - self.contract_address - } - - pub async fn quote_convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { - self.call_gateway_contract(IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }) - .await - } - - pub async fn quote_convert_to_assets(&self, yo_vault: Address, shares: U256) -> Result { - self.call_gateway_contract(IYoGateway::quoteConvertToAssetsCall { yoVault: yo_vault, shares }) - .await - } - - pub async fn quote_preview_deposit(&self, yo_vault: Address, assets: U256) -> Result { - self.call_gateway_contract(IYoGateway::quotePreviewDepositCall { yoVault: yo_vault, assets }) - .await - } - - pub async fn quote_preview_redeem(&self, yo_vault: Address, shares: U256) -> Result { - self.call_gateway_contract(IYoGateway::quotePreviewRedeemCall { yoVault: yo_vault, shares }) - .await - } - - pub async fn get_asset_allowance(&self, yo_vault: Address, owner: Address) -> Result { - self.call_gateway_contract(IYoGateway::getAssetAllowanceCall { yoVault: yo_vault, owner }).await - } - - pub async fn get_share_allowance(&self, yo_vault: Address, owner: Address) -> Result { - self.call_gateway_contract(IYoGateway::getShareAllowanceCall { yoVault: yo_vault, owner }).await - } - - pub async fn quote_convert_to_shares_for(&self, vault: YoVault, assets: U256) -> Result { - self.quote_convert_to_shares(vault.yo_token, assets).await - } - - pub async fn quote_convert_to_assets_for(&self, vault: YoVault, shares: U256) -> Result { - self.quote_convert_to_assets(vault.yo_token, shares).await - } - - pub async fn quote_preview_deposit_for(&self, vault: YoVault, assets: U256) -> Result { - self.quote_preview_deposit(vault.yo_token, assets).await - } - - pub async fn quote_preview_redeem_for(&self, vault: YoVault, shares: U256) -> Result { - self.quote_preview_redeem(vault.yo_token, shares).await - } - - pub async fn get_asset_allowance_for(&self, vault: YoVault, owner: Address) -> Result { - self.get_asset_allowance(vault.yo_token, owner).await - } - - pub async fn get_share_allowance_for(&self, vault: YoVault, owner: Address) -> Result { - self.get_share_allowance(vault.yo_token, owner).await - } - - pub fn deposit_call_data(yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { + fn deposit_call_data(yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { IYoGateway::depositCall { yoVault: yo_vault, assets, @@ -130,7 +60,7 @@ impl YoGatewayClient { .abi_encode() } - pub fn redeem_call_data(yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { + fn redeem_call_data(yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { IYoGateway::redeemCall { yoVault: yo_vault, shares, @@ -140,82 +70,6 @@ impl YoGatewayClient { } .abi_encode() } - - pub fn deposit_call_data_for(vault: YoVault, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { - Self::deposit_call_data(vault.yo_token, assets, min_shares_out, receiver, partner_id) - } - - pub fn redeem_call_data_for(vault: YoVault, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { - Self::redeem_call_data(vault.yo_token, shares, min_assets_out, receiver, partner_id) - } - - pub fn build_deposit_transaction( - &self, - from: Address, - yo_vault: Address, - assets: U256, - min_shares_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject { - let data = Self::deposit_call_data(yo_vault, assets, min_shares_out, receiver, partner_id); - TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) - } - - pub fn build_redeem_transaction( - &self, - from: Address, - yo_vault: Address, - shares: U256, - min_assets_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject { - let data = Self::redeem_call_data(yo_vault, shares, min_assets_out, receiver, partner_id); - TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) - } - - async fn call_gateway_contract(&self, call: Call) -> Result - where - Call: SolCall, - { - self.call_contract_at_block(call, self.contract_address, None).await - } - - async fn call_contract_at_block(&self, call: Call, contract: Address, block_number: Option) -> Result - where - Call: SolCall, - { - let payload = hex::encode_prefixed(call.abi_encode()); - let contract_address = contract.to_string(); - - let block_param = block_number - .map(|number| format!("0x{number:x}")) - .map_or_else(|| json!("latest"), serde_json::Value::String); - - let response: String = self - .ethereum_client - .client - .call( - "eth_call", - json!([ - { - "to": contract_address, - "data": payload, - }, - block_param - ]), - ) - .await - .map_err(|err| YieldError::new(format!("yo gateway rpc call failed: {err}")))?; - - if response.trim().is_empty() || response == "0x" { - return Err(YieldError::new("yo gateway response did not contain data")); - } - - let decoded = hex::decode(&response).map_err(|err| YieldError::new(format!("invalid hex returned by yo gateway: {err}")))?; - Call::abi_decode_returns(&decoded).map_err(|err| YieldError::new(format!("failed to decode yo gateway response: {err}"))) - } } #[async_trait] @@ -227,10 +81,6 @@ where self.contract_address } - fn chain(&self) -> Chain { - self.ethereum_client.get_chain() - } - fn build_deposit_transaction( &self, from: Address, @@ -240,7 +90,8 @@ where receiver: Address, partner_id: u32, ) -> TransactionObject { - >::build_deposit_transaction(self, from, yo_vault, assets, min_shares_out, receiver, partner_id) + let data = Self::deposit_call_data(yo_vault, 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( @@ -252,58 +103,17 @@ where receiver: Address, partner_id: u32, ) -> TransactionObject { - >::build_redeem_transaction(self, from, yo_vault, shares, min_assets_out, receiver, partner_id) + let data = Self::redeem_call_data(yo_vault, shares, min_assets_out, receiver, partner_id); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn balance_of(&self, token: Address, owner: Address) -> Result { - let call = IERC20::balanceOfCall { account: owner }.abi_encode(); - let payload = hex::encode_prefixed(call); - let params = json!([ - { - "to": token.to_string(), - "data": payload, - }, - "latest" - ]); - - let result: String = self + async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { + let latest_block = self .ethereum_client - .client - .call("eth_call", params) - .await - .map_err(|err| YieldError::new(format!("yo gateway rpc call failed: {err}")))?; - - let value = result.trim_start_matches("0x"); - U256::from_str_radix(value, 16).map_err(|err| YieldError::new(format!("invalid balance data: {err}"))) - } - - async fn convert_to_assets_at_block(&self, yo_vault: Address, shares: U256, block_number: u64) -> Result { - self.call_contract_at_block(IYoVaultToken::convertToAssetsCall { shares }, yo_vault, Some(block_number)) - .await - } - - async fn latest_block_number(&self) -> Result { - self.ethereum_client .get_latest_block() .await - .map_err(|err| YieldError::new(format!("yo gateway failed to fetch latest block: {err}"))) - } + .map_err(|err| YieldError::new(format!("failed to fetch latest block: {err}")))?; - async fn block_timestamp(&self, block_number: u64) -> Result { - let block = self - .ethereum_client - .get_block(block_number) - .await - .map_err(|err| YieldError::new(format!("yo gateway failed to fetch block {block_number}: {err}")))?; - - block - .timestamp - .to_u64() - .ok_or_else(|| YieldError::new(format!("yo gateway failed to parse timestamp for block {block_number}"))) - } - - async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { - let latest_block = self.latest_block_number().await?; let lookback_block = latest_block.saturating_sub(lookback_blocks); let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); let multicall_addr: Address = gem_evm::multicall3::deployment_by_chain_stack(self.ethereum_client.chain.chain_stack()) diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 94c5c0898..446677d73 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -7,12 +7,12 @@ mod vault; pub use client::{YoGatewayClient, YoProvider}; pub use contract::{IYoGateway, IYoVaultToken}; -pub use model::PositionData; pub use error::YieldError; +pub use model::PositionData; pub use provider::YoYieldProvider; -pub use vault::{YO_USD, YoVault, vaults}; +pub use vault::{YO_USD, YO_USDT, YoVault, vaults}; -use alloy_primitives::{Address, address}; +use alloy_primitives::{address, Address}; -pub const YO_GATEWAY_BASE_MAINNET: Address = address!("0xF1EeE0957267b1A474323Ff9CfF7719E964969FA"); +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 index f6172c654..8252af125 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -1,9 +1,9 @@ -use std::{str::FromStr, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; -use primitives::AssetId; +use primitives::{AssetId, Chain}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::provider::YieldProviderClient; @@ -12,20 +12,27 @@ use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, v const SECONDS_PER_YEAR: f64 = 31_536_000.0; -// Base chain has ~2 second block time, 7 days lookback -const LOOKBACK_BLOCKS: u64 = 7 * 24 * 60 * 60 / 2; +fn lookback_blocks_for_chain(chain: Chain) -> u64 { + match chain { + // Base chain has ~2 second block time, 7 days lookback + Chain::Base => 7 * 24 * 60 * 60 / 2, + // Ethereum has ~12 second block time, 7 days lookback + Chain::Ethereum => 7 * 24 * 60 * 60 / 12, + _ => 7 * 24 * 60 * 60 / 12, // Default to Ethereum-like + } +} #[derive(Clone)] pub struct YoYieldProvider { vaults: Vec, - gateway: Arc, + gateways: HashMap>, } impl YoYieldProvider { - pub fn new(gateway: Arc) -> Self { + pub fn new(gateways: HashMap>) -> Self { Self { vaults: vaults().to_vec(), - gateway, + gateways, } } @@ -36,6 +43,12 @@ impl YoYieldProvider { .find(|vault| vault.asset_id() == *asset_id) .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) } + + fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YieldError> { + self.gateways + .get(&chain) + .ok_or_else(|| YieldError::new(format!("no gateway configured for chain {:?}", chain))) + } } #[async_trait] @@ -62,7 +75,9 @@ impl YieldProviderClient for YoYieldProvider { let mut results = Vec::new(); for vault in self.vaults.iter().copied().filter(|vault| vault.asset_id() == *asset_id) { - let data = self.gateway.fetch_position_data(vault, Address::ZERO, LOOKBACK_BLOCKS).await?; + let gateway = self.gateway_for_chain(vault.chain)?; + let lookback_blocks = lookback_blocks_for_chain(vault.chain); + let data = gateway.fetch_position_data(vault, Address::ZERO, lookback_blocks).await?; let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); @@ -73,38 +88,38 @@ impl YieldProviderClient for YoYieldProvider { async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { let vault = self.find_vault(asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; let wallet = parse_address(wallet_address)?; let receiver = wallet; let amount = parse_value(value)?; let min_shares = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; - let tx = self - .gateway - .build_deposit_transaction(wallet, vault.yo_token, amount, min_shares, receiver, partner_id); + let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, min_shares, receiver, partner_id); Ok(convert_transaction(vault, tx)) } async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { let vault = self.find_vault(asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; let wallet = parse_address(wallet_address)?; let receiver = wallet; let shares = parse_value(value)?; let min_assets = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; - let tx = self - .gateway - .build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); + let tx = gateway.build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); Ok(convert_transaction(vault, tx)) } async fn positions(&self, request: &YieldDetailsRequest) -> Result { let vault = self.find_vault(&request.asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; + let lookback_blocks = lookback_blocks_for_chain(vault.chain); let owner = parse_address(&request.wallet_address)?; let mut details = YieldPosition::new(vault.name, request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); - let data = self.gateway.fetch_position_data(vault, owner, LOOKBACK_BLOCKS).await?; + let data = gateway.fetch_position_data(vault, owner, lookback_blocks).await?; details.vault_balance_value = Some(data.share_balance.to_string()); diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index a846a9e46..ed123b30f 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -34,6 +34,14 @@ pub const YO_USD: YoVault = YoVault::new( 6, ); +pub const YO_USDT: YoVault = YoVault::new( + "yoUSDT", + Chain::Ethereum, + address!("0xb9a7da9e90d3b428083bae04b860faa6325b721e"), + address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), + 6, +); + pub fn vaults() -> &'static [YoVault] { - &[YO_USD] + &[YO_USD, YO_USDT] } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index c70f09157..85cce7348 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -1,7 +1,7 @@ mod remote_types; pub use remote_types::*; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use crate::{ GemstoneError, @@ -11,7 +11,9 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{YO_GATEWAY_BASE_MAINNET, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{ + YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider, +}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -63,14 +65,27 @@ impl GemYielder { } fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { - let endpoint = rpc_provider.get_endpoint(Chain::Base)?; - let wrapper = AlienProviderWrapper { provider: rpc_provider }; - let rpc_client = RpcClient::new(endpoint, Arc::new(wrapper)); - let jsonrpc_client = JsonRpcClient::new(rpc_client); - let evm_chain = EVMChain::Base; - let ethereum_client = EthereumClient::new(jsonrpc_client, evm_chain); - let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); - let gateway: Arc = Arc::new(gateway_client); - let provider: Arc = Arc::new(YoYieldProvider::new(gateway)); + let wrapper = Arc::new(AlienProviderWrapper { + provider: rpc_provider.clone(), + }); + let mut gateways: HashMap> = HashMap::new(); + + // Base gateway + let base_endpoint = rpc_provider.get_endpoint(Chain::Base)?; + let base_rpc_client = RpcClient::new(base_endpoint, wrapper.clone()); + let base_jsonrpc_client = JsonRpcClient::new(base_rpc_client); + let base_ethereum_client = EthereumClient::new(base_jsonrpc_client, EVMChain::Base); + let base_gateway: Arc = Arc::new(YoGatewayClient::new(base_ethereum_client, YO_GATEWAY)); + gateways.insert(Chain::Base, base_gateway); + + // Ethereum gateway + let eth_endpoint = rpc_provider.get_endpoint(Chain::Ethereum)?; + let eth_rpc_client = RpcClient::new(eth_endpoint, wrapper); + let eth_jsonrpc_client = JsonRpcClient::new(eth_rpc_client); + let eth_ethereum_client = EthereumClient::new(eth_jsonrpc_client, EVMChain::Ethereum); + let eth_gateway: Arc = Arc::new(YoGatewayClient::new(eth_ethereum_client, YO_GATEWAY)); + gateways.insert(Chain::Ethereum, eth_gateway); + + let provider: Arc = Arc::new(YoYieldProvider::new(gateways)); Ok(provider) } From 16dfa29347b4b9bb0eaf916bca6ab5ad86e0b68a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:46:26 +0900 Subject: [PATCH 10/33] add yield build_transaction --- crates/gem_evm/src/provider/preload_mapper.rs | 6 ++- crates/yielder/src/yo/client.rs | 41 +++++++++++++----- gemstone/src/gateway/mod.rs | 5 +-- gemstone/src/gem_yielder/mod.rs | 43 +++++++++++++++++++ gemstone/src/gem_yielder/remote_types.rs | 10 +++++ gemstone/src/models/transaction.rs | 20 ++++++++- 6 files changed, 109 insertions(+), 16 deletions(-) diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index ec95639ef..f9cc7f33e 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -63,7 +63,9 @@ pub fn map_transaction_fee_rates(chain: EVMChain, fee_history: &EthereumFeeHisto .into_iter() .map(|x| { let priority_fee = BigInt::max(min_priority_fee.clone(), x.value.clone()); - FeeRate::new(x.priority, GasPriceType::eip1559(base_fee.clone(), priority_fee)) + // maxFeePerGas must be >= maxPriorityFeePerGas, so use base_fee + priority_fee + let max_fee_per_gas = base_fee.clone() + &priority_fee; + FeeRate::new(x.priority, GasPriceType::eip1559(max_fee_per_gas, priority_fee)) }) .collect()) } @@ -375,6 +377,8 @@ mod tests { GasPriceType::Eip1559 { gas_price, priority_fee } => { assert!(*gas_price >= min_priority_fee); assert!(*priority_fee >= min_priority_fee); + // EIP-1559: maxFeePerGas must be >= maxPriorityFeePerGas + assert!(*gas_price >= *priority_fee, "maxFeePerGas must be >= maxPriorityFeePerGas"); } _ => panic!("Expected EIP-1559 gas price type"), } diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 85ffa0698..c79a8e1d7 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -70,6 +70,18 @@ impl YoGatewayClient { } .abi_encode() } + + async fn fetch_lookback_data(&self, yo_token: Address, one_share: U256, multicall_addr: Address, lookback_block: u64) -> Result<(U256, u64), YieldError> { + let mut lookback_batch = self.ethereum_client.multicall(); + let lookback_price_call = lookback_batch.add(yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); + let lookback_ts = lookback_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); + + let lookback = lookback_batch.at_block(lookback_block).execute().await?; + let price = lookback.decode::(&lookback_price_call)?; + let timestamp = lookback.decode::(&lookback_ts)?.to::(); + + Ok((price, timestamp)) + } } #[async_trait] @@ -123,22 +135,29 @@ where let mut latest_batch = self.ethereum_client.multicall(); let share_bal = latest_batch.add(vault.yo_token, IERC20::balanceOfCall { account: owner }); let asset_bal = latest_batch.add(vault.asset_token, IERC20::balanceOfCall { account: owner }); - let latest_price = latest_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); + let latest_price_call = latest_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); let latest_ts = latest_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); - let mut lookback_batch = self.ethereum_client.multicall(); - let lookback_price = lookback_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); - let lookback_ts = lookback_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); + let latest = latest_batch.at_block(latest_block).execute().await?; - let (latest, lookback) = tokio::try_join!(latest_batch.at_block(latest_block).execute(), lookback_batch.at_block(lookback_block).execute())?; + let share_balance = latest.decode::(&share_bal)?; + let asset_balance = latest.decode::(&asset_bal)?; + let latest_price = latest.decode::(&latest_price_call)?; + let latest_timestamp = latest.decode::(&latest_ts)?.to::(); + + // Lookback query may fail if vault didn't exist at that block - use latest as fallback + let (lookback_price, lookback_timestamp) = self + .fetch_lookback_data(vault.yo_token, one_share, multicall_addr, lookback_block) + .await + .unwrap_or((latest_price, latest_timestamp)); Ok(PositionData { - share_balance: latest.decode::(&share_bal)?, - asset_balance: latest.decode::(&asset_bal)?, - latest_price: latest.decode::(&latest_price)?, - latest_timestamp: latest.decode::(&latest_ts)?.to::(), - lookback_price: lookback.decode::(&lookback_price)?, - lookback_timestamp: lookback.decode::(&lookback_ts)?.to::(), + share_balance, + asset_balance, + latest_price, + latest_timestamp, + lookback_price, + lookback_timestamp, }) } } diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index a3004f051..59b15c240 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -278,9 +278,8 @@ impl GemGateway { pub async fn get_transaction_preload(&self, chain: Chain, input: GemTransactionPreloadInput) -> Result { let preload_input: primitives::TransactionPreloadInput = input.into(); - let metadata = self - .provider(chain) - .await? + let provider = self.provider(chain).await?; + let metadata = provider .get_transaction_preload(preload_input) .await .map_err(|e| GatewayError::NetworkError { msg: e.to_string() })?; diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 85cce7348..e2f2655b8 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -62,6 +62,49 @@ impl GemYielder { }; self.yielder.positions(provider, &request).await.map_err(Into::into) } + + /// Build a complete yield transaction with all data needed for signing. + /// This method combines the yield transaction building with metadata. + /// + /// # Arguments + /// * `action` - Whether to deposit or withdraw + /// * `provider` - The yield provider name (e.g., "yo") + /// * `asset` - The asset to deposit/withdraw + /// * `wallet_address` - The wallet address performing the action + /// * `value` - The amount to deposit/withdraw + /// * `nonce` - The transaction nonce from preload + /// * `chain_id` - The chain ID from preload + pub async fn build_transaction( + &self, + action: GemYieldAction, + provider: String, + asset: AssetId, + wallet_address: String, + value: String, + nonce: u64, + chain_id: u64, + ) -> Result { + let provider = provider.parse::()?; + + let transaction = match action { + GemYieldAction::Deposit => { + self.yielder.deposit(provider, &asset, &wallet_address, &value).await? + } + GemYieldAction::Withdraw => { + self.yielder.withdraw(provider, &asset, &wallet_address, &value).await? + } + }; + + // Default gas limit for yield operations (deposit/withdraw to ERC4626 vaults) + let gas_limit = "200000".to_string(); + + Ok(GemYieldTransactionData { + transaction, + nonce, + chain_id, + gas_limit, + }) + } } fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index 385506932..d1b021ea0 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,6 +1,8 @@ use primitives::AssetId; use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; +pub use crate::models::transaction::GemYieldAction; + pub type GemYieldProvider = YieldProvider; #[uniffi::remote(Enum)] @@ -8,6 +10,14 @@ pub enum GemYieldProvider { Yo, } +#[derive(Debug, Clone, uniffi::Record)] +pub struct GemYieldTransactionData { + pub transaction: GemYieldTransaction, + pub nonce: u64, + pub chain_id: u64, + pub gas_limit: String, +} + pub type GemYield = Yield; #[uniffi::remote(Record)] diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index f306a7cca..fee34a01a 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -255,6 +255,17 @@ pub enum PerpetualType { Reduce(PerpetualReduceData), } +#[derive(Debug, Clone, uniffi::Enum)] +pub enum GemYieldAction { + Deposit, + Withdraw, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct GemYieldData { + pub provider_name: String, +} + #[derive(Debug, Clone, uniffi::Enum)] #[allow(clippy::large_enum_variant)] pub enum GemTransactionInputType { @@ -294,6 +305,11 @@ pub enum GemTransactionInputType { asset: GemAsset, perpetual_type: GemPerpetualType, }, + Yield { + asset: GemAsset, + action: GemYieldAction, + data: GemYieldData, + }, } impl GemTransactionInputType { @@ -306,7 +322,8 @@ impl GemTransactionInputType { | Self::Generic { asset, .. } | Self::TransferNft { asset, .. } | Self::Account { asset, .. } - | Self::Perpetual { asset, .. } => asset, + | Self::Perpetual { asset, .. } + | Self::Yield { asset, .. } => asset, Self::Swap { from_asset, .. } => from_asset, } } @@ -854,6 +871,7 @@ impl From for TransactionInputType { GemTransactionInputType::TransferNft { asset, nft_asset } => TransactionInputType::TransferNft(asset, nft_asset), GemTransactionInputType::Account { asset, account_type } => TransactionInputType::Account(asset, account_type), GemTransactionInputType::Perpetual { asset, perpetual_type } => TransactionInputType::Perpetual(asset, perpetual_type), + GemTransactionInputType::Yield { asset, .. } => TransactionInputType::Deposit(asset), } } } From 72815dc45891077e60fbf9ed7d7100028c4d94c3 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:17:48 +0900 Subject: [PATCH 11/33] handle TransactionInputType::Yield in Gateway and preload --- crates/gem_aptos/src/rpc/client.rs | 3 +- .../gem_cosmos/src/provider/preload_mapper.rs | 18 ++- crates/gem_evm/src/provider/preload.rs | 9 +- crates/gem_evm/src/provider/preload_mapper.rs | 12 +- .../gem_solana/src/provider/preload_mapper.rs | 6 +- crates/gem_sui/src/provider/preload_mapper.rs | 3 +- crates/primitives/src/lib.rs | 2 + .../primitives/src/transaction_input_type.rs | 8 ++ crates/primitives/src/yield_data.rs | 17 +++ gemstone/src/gateway/mod.rs | 12 ++ gemstone/src/gem_yielder/mod.rs | 109 +++++++++++------- gemstone/src/lib.rs | 6 + gemstone/src/models/transaction.rs | 47 +++++++- 13 files changed, 191 insertions(+), 61 deletions(-) create mode 100644 crates/primitives/src/yield_data.rs diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index 6a7ef82e1..f28fcae86 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -132,7 +132,8 @@ impl AptosClient { TransactionInputType::Swap(_, _, _) | TransactionInputType::Stake(_, _) | TransactionInputType::TokenApprove(_, _) - | TransactionInputType::Generic(_, _, _) => Ok(1500), + | TransactionInputType::Generic(_, _, _) + | TransactionInputType::Yield(_, _, _) => Ok(1500), TransactionInputType::Perpetual(_, _) => unimplemented!(), } } diff --git a/crates/gem_cosmos/src/provider/preload_mapper.rs b/crates/gem_cosmos/src/provider/preload_mapper.rs index 3a9913803..db0d467cb 100644 --- a/crates/gem_cosmos/src/provider/preload_mapper.rs +++ b/crates/gem_cosmos/src/provider/preload_mapper.rs @@ -11,7 +11,8 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(3_000u64), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(25_000u64), }, @@ -22,7 +23,8 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(10_000u64), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(10_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(10_000u64), TransactionInputType::Stake(_, _) => BigInt::from(100_000u64), }, @@ -33,7 +35,8 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(3_000u64), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(10_000u64), }, @@ -44,7 +47,8 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(100_000u64), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(100_000u64), TransactionInputType::Stake(_, _) => BigInt::from(200_000u64), }, @@ -55,7 +59,8 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(100_000_000_000_000u64), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000_000_000_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(100_000_000_000_000u64), TransactionInputType::Stake(_, _) => BigInt::from(1_000_000_000_000_000u64), }, @@ -71,7 +76,8 @@ fn get_gas_limit(input_type: &TransactionInputType, _chain: CosmosChain) -> u64 | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => 200_000, + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => 200_000, TransactionInputType::Swap(_, _, _) => 200_000, TransactionInputType::Stake(_, operation) => match operation { StakeType::Stake(_) | StakeType::Unstake(_) => 1_000_000, diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index 7eb7b2106..f7452a3e2 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -65,8 +65,8 @@ impl EthereumClient { let gas_limit = calculate_gas_limit_with_increase(gas_estimate); let fee = self.calculate_fee(&input, &gas_limit).await?; - let metadata = if let TransactionInputType::Stake(_, _) = &input.input_type { - match input.metadata { + let metadata = match &input.input_type { + TransactionInputType::Stake(_, _) | TransactionInputType::Yield(_, _, _) => match input.metadata { TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { nonce, chain_id, @@ -76,9 +76,8 @@ impl EthereumClient { }), }, _ => input.metadata, - } - } else { - input.metadata + }, + _ => input.metadata, }; Ok(TransactionLoadData { fee, metadata }) diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index f9cc7f33e..c8f9f8e43 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -8,8 +8,8 @@ use num_bigint::BigInt; use num_traits::Num; use primitives::swap::SwapQuoteDataType; use primitives::{ - AssetSubtype, Chain, EVMChain, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, fee::FeePriority, - fee::GasPriceType, + AssetSubtype, Chain, EVMChain, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, YieldAction, + fee::FeePriority, fee::GasPriceType, }; use crate::contracts::{IERC20, IERC721, IERC1155}; @@ -160,6 +160,14 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> } _ => Err("Unsupported chain for staking".into()), }, + TransactionInputType::Yield(_, action, yield_data) => { + let call_data = alloy_primitives::hex::decode(&yield_data.call_data)?; + let tx_value = match action { + YieldAction::Deposit => BigInt::from(0), + YieldAction::Withdraw => BigInt::from(0), + }; + Ok(TransactionParams::new(yield_data.contract_address.clone(), call_data, tx_value)) + } _ => Err("Unsupported transfer type".into()), } } diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 949cc35b7..04da80dc1 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -40,7 +40,8 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt { | TransactionInputType::Account(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => BigInt::from(100_000), + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000), TransactionInputType::Swap(_, _, _) => BigInt::from(420_000), TransactionInputType::Stake(_, _) => BigInt::from(100_000), } @@ -54,7 +55,8 @@ fn get_multiple_of(input_type: &TransactionInputType) -> i64 { | TransactionInputType::Account(asset, _) | TransactionInputType::TokenApprove(asset, _) | TransactionInputType::Generic(asset, _, _) - | TransactionInputType::Perpetual(asset, _) => match &asset.id.token_subtype() { + | TransactionInputType::Perpetual(asset, _) + | TransactionInputType::Yield(asset, _, _) => match &asset.id.token_subtype() { AssetSubtype::NATIVE => 25_000, AssetSubtype::TOKEN => 50_000, }, diff --git a/crates/gem_sui/src/provider/preload_mapper.rs b/crates/gem_sui/src/provider/preload_mapper.rs index b482c0f4f..7751ee699 100644 --- a/crates/gem_sui/src/provider/preload_mapper.rs +++ b/crates/gem_sui/src/provider/preload_mapper.rs @@ -40,7 +40,8 @@ fn get_gas_limit(input_type: &TransactionInputType) -> u64 { | TransactionInputType::Deposit(_) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => GAS_BUDGET, + | TransactionInputType::Perpetual(_, _) + | TransactionInputType::Yield(_, _, _) => GAS_BUDGET, TransactionInputType::Swap(_, _, _) => 50_000_000, TransactionInputType::Stake(_, _) => GAS_BUDGET, } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 5f0c8777d..e9437b9ca 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -229,6 +229,8 @@ pub mod transaction_input_type; pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData, TransactionLoadInput}; pub mod transfer_data_extra; pub use self::transfer_data_extra::TransferDataExtra; +pub mod yield_data; +pub use self::yield_data::{YieldAction, YieldData}; pub mod transaction_data_output; pub use self::transaction_data_output::{TransferDataOutputAction, TransferDataOutputType}; pub mod broadcast_options; diff --git a/crates/primitives/src/transaction_input_type.rs b/crates/primitives/src/transaction_input_type.rs index 19133c248..b6018a8cc 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -2,6 +2,7 @@ use crate::stake_type::StakeType; use crate::swap::{ApprovalData, SwapData}; use crate::transaction_fee::TransactionFee; use crate::transaction_load_metadata::TransactionLoadMetadata; +use crate::yield_data::{YieldAction, YieldData}; use crate::{ Asset, GasPriceType, PerpetualType, TransactionPreloadInput, TransactionType, TransferDataExtra, WalletConnectionSessionAppMetadata, nft::NFTAsset, perpetual::AccountDataType, @@ -22,6 +23,7 @@ pub enum TransactionInputType { TransferNft(Asset, NFTAsset), Account(Asset, AccountDataType), Perpetual(Asset, PerpetualType), + Yield(Asset, YieldAction, YieldData), } impl TransactionInputType { @@ -36,6 +38,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, + TransactionInputType::Yield(asset, _, _) => asset, } } @@ -50,6 +53,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, + TransactionInputType::Yield(asset, _, _) => asset, } } @@ -74,6 +78,10 @@ impl TransactionInputType { PerpetualType::Close(_) | PerpetualType::Reduce(_) => TransactionType::PerpetualClosePosition, PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, + TransactionInputType::Yield(_, action, _) => match action { + YieldAction::Deposit => TransactionType::StakeDelegate, + YieldAction::Withdraw => TransactionType::StakeUndelegate, + }, } } } diff --git a/crates/primitives/src/yield_data.rs b/crates/primitives/src/yield_data.rs new file mode 100644 index 000000000..bee024c86 --- /dev/null +++ b/crates/primitives/src/yield_data.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +pub enum YieldAction { + Deposit, + Withdraw, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +pub struct YieldData { + pub provider_name: String, + pub contract_address: String, + pub call_data: String, +} diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 59b15c240..8ba0b9c3f 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -10,6 +10,7 @@ use preferences::PreferencesWrapper; use crate::alien::{AlienProvider, new_alien_client}; use crate::api_client::GemApiClient; +use crate::gem_yielder::{build_yielder, prepare_yield_input}; use crate::models::*; use crate::network::JsonRpcClient; use chain_traits::ChainTraits; @@ -32,6 +33,7 @@ use gem_xrp::rpc::client::XRPClient; use std::sync::Arc; use primitives::{BitcoinChain, Chain, ChartPeriod, EVMChain, ScanAddressTarget, ScanTransactionPayload, TransactionPreloadInput, chain_cosmos::CosmosChain}; +use yielder::Yielder; #[uniffi::export(with_foreign)] #[async_trait::async_trait] @@ -46,6 +48,7 @@ pub struct GemGateway { pub preferences: Arc, pub secure_preferences: Arc, pub api_client: GemApiClient, + yielder: Option, } impl std::fmt::Debug for GemGateway { @@ -150,11 +153,13 @@ 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 yielder = build_yielder(provider.clone()).ok(); Self { provider, preferences, secure_preferences, api_client, + yielder, } } @@ -341,6 +346,13 @@ impl GemGateway { input: GemTransactionLoadInput, provider: Arc, ) -> Result { + // Prepare yield input (builds contract_address and call_data if needed) + let input = if let Some(yielder) = &self.yielder { + prepare_yield_input(yielder, input).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() })? + } else { + input + }; + let fee = self.get_fee(chain, input.clone(), provider.clone()).await?; let load_data = self diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index e2f2655b8..68c8ad50c 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ GemstoneError, alien::{AlienProvider, AlienProviderWrapper}, + models::{GemTransactionInputType, GemTransactionLoadInput, GemYieldData}, }; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; @@ -30,10 +31,8 @@ impl std::fmt::Debug for GemYielder { impl GemYielder { #[uniffi::constructor] pub fn new(rpc_provider: Arc) -> Result { - let mut inner = Yielder::new(); - let yo_provider = build_yo_provider(rpc_provider)?; - inner.add_provider_arc(yo_provider); - Ok(Self { yielder: inner }) + let yielder = build_yielder(rpc_provider)?; + Ok(Self { yielder }) } pub async fn yields_for_asset(&self, asset_id: &AssetId) -> Result, GemstoneError> { @@ -63,17 +62,6 @@ impl GemYielder { self.yielder.positions(provider, &request).await.map_err(Into::into) } - /// Build a complete yield transaction with all data needed for signing. - /// This method combines the yield transaction building with metadata. - /// - /// # Arguments - /// * `action` - Whether to deposit or withdraw - /// * `provider` - The yield provider name (e.g., "yo") - /// * `asset` - The asset to deposit/withdraw - /// * `wallet_address` - The wallet address performing the action - /// * `value` - The amount to deposit/withdraw - /// * `nonce` - The transaction nonce from preload - /// * `chain_id` - The chain ID from preload pub async fn build_transaction( &self, action: GemYieldAction, @@ -95,40 +83,75 @@ impl GemYielder { } }; - // Default gas limit for yield operations (deposit/withdraw to ERC4626 vaults) - let gas_limit = "200000".to_string(); - Ok(GemYieldTransactionData { transaction, nonce, chain_id, - gas_limit, + gas_limit: "200000".to_string(), }) } + +} + +pub(crate) fn build_yielder(rpc_provider: Arc) -> Result { + let wrapper = Arc::new(AlienProviderWrapper { provider: rpc_provider.clone() }); + + let build_gateway = |chain: Chain, evm_chain: EVMChain| -> Result, GemstoneError> { + let endpoint = rpc_provider.get_endpoint(chain)?; + let rpc_client = RpcClient::new(endpoint, wrapper.clone()); + let ethereum_client = EthereumClient::new(JsonRpcClient::new(rpc_client), evm_chain); + Ok(Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY))) + }; + + let gateways: HashMap> = HashMap::from([ + (Chain::Base, build_gateway(Chain::Base, EVMChain::Base)?), + (Chain::Ethereum, build_gateway(Chain::Ethereum, EVMChain::Ethereum)?), + ]); + + let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways)); + let mut yielder = Yielder::new(); + yielder.add_provider_arc(yo_provider); + Ok(yielder) } -fn build_yo_provider(rpc_provider: Arc) -> Result, GemstoneError> { - let wrapper = Arc::new(AlienProviderWrapper { - provider: rpc_provider.clone(), - }); - let mut gateways: HashMap> = HashMap::new(); - - // Base gateway - let base_endpoint = rpc_provider.get_endpoint(Chain::Base)?; - let base_rpc_client = RpcClient::new(base_endpoint, wrapper.clone()); - let base_jsonrpc_client = JsonRpcClient::new(base_rpc_client); - let base_ethereum_client = EthereumClient::new(base_jsonrpc_client, EVMChain::Base); - let base_gateway: Arc = Arc::new(YoGatewayClient::new(base_ethereum_client, YO_GATEWAY)); - gateways.insert(Chain::Base, base_gateway); - - // Ethereum gateway - let eth_endpoint = rpc_provider.get_endpoint(Chain::Ethereum)?; - let eth_rpc_client = RpcClient::new(eth_endpoint, wrapper); - let eth_jsonrpc_client = JsonRpcClient::new(eth_rpc_client); - let eth_ethereum_client = EthereumClient::new(eth_jsonrpc_client, EVMChain::Ethereum); - let eth_gateway: Arc = Arc::new(YoGatewayClient::new(eth_ethereum_client, YO_GATEWAY)); - gateways.insert(Chain::Ethereum, eth_gateway); - - let provider: Arc = Arc::new(YoYieldProvider::new(gateways)); - Ok(provider) +pub(crate) async fn prepare_yield_input( + yielder: &Yielder, + input: GemTransactionLoadInput, +) -> Result { + match &input.input_type { + GemTransactionInputType::Yield { asset, action, data } => { + if data.contract_address.is_empty() || data.call_data.is_empty() { + let transaction = match action { + GemYieldAction::Deposit => { + yielder.deposit(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await? + } + GemYieldAction::Withdraw => { + yielder.withdraw(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await? + } + }; + + Ok(GemTransactionLoadInput { + input_type: GemTransactionInputType::Yield { + asset: asset.clone(), + action: action.clone(), + data: GemYieldData { + provider_name: data.provider_name.clone(), + contract_address: transaction.to, + call_data: transaction.data, + }, + }, + sender_address: input.sender_address, + destination_address: input.destination_address, + value: input.value, + gas_price: input.gas_price, + memo: input.memo, + is_max_value: input.is_max_value, + metadata: input.metadata, + }) + } else { + Ok(input) + } + } + _ => Ok(input), + } } diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index f0e1a56eb..153cb3ee4 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -113,3 +113,9 @@ impl From for GemstoneError { Self::AnyError { msg: error.to_string() } } } + +impl From for GemstoneError { + fn from(error: gateway::GatewayError) -> Self { + Self::AnyError { msg: error.to_string() } + } +} diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index fee34a01a..cdbf6adb8 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -264,6 +264,8 @@ pub enum GemYieldAction { #[derive(Debug, Clone, uniffi::Record)] pub struct GemYieldData { pub provider_name: String, + pub contract_address: String, + pub call_data: String, } #[derive(Debug, Clone, uniffi::Enum)] @@ -709,6 +711,11 @@ impl From for GemTransactionInputType { TransactionInputType::TransferNft(asset, nft_asset) => GemTransactionInputType::TransferNft { asset, nft_asset }, TransactionInputType::Account(asset, account_type) => GemTransactionInputType::Account { asset, account_type }, TransactionInputType::Perpetual(asset, perpetual_type) => GemTransactionInputType::Perpetual { asset, perpetual_type }, + TransactionInputType::Yield(asset, action, data) => GemTransactionInputType::Yield { + asset, + action: action.into(), + data: data.into(), + }, } } } @@ -871,7 +878,7 @@ impl From for TransactionInputType { GemTransactionInputType::TransferNft { asset, nft_asset } => TransactionInputType::TransferNft(asset, nft_asset), GemTransactionInputType::Account { asset, account_type } => TransactionInputType::Account(asset, account_type), GemTransactionInputType::Perpetual { asset, perpetual_type } => TransactionInputType::Perpetual(asset, perpetual_type), - GemTransactionInputType::Yield { asset, .. } => TransactionInputType::Deposit(asset), + GemTransactionInputType::Yield { asset, action, data } => TransactionInputType::Yield(asset, action.into(), data.into()), } } } @@ -893,3 +900,41 @@ impl From for GemFreezeData { } } } + +impl From for primitives::YieldAction { + fn from(value: GemYieldAction) -> Self { + match value { + GemYieldAction::Deposit => primitives::YieldAction::Deposit, + GemYieldAction::Withdraw => primitives::YieldAction::Withdraw, + } + } +} + +impl From for GemYieldAction { + fn from(value: primitives::YieldAction) -> Self { + match value { + primitives::YieldAction::Deposit => GemYieldAction::Deposit, + primitives::YieldAction::Withdraw => GemYieldAction::Withdraw, + } + } +} + +impl From for primitives::YieldData { + fn from(value: GemYieldData) -> Self { + primitives::YieldData { + provider_name: value.provider_name, + contract_address: value.contract_address, + call_data: value.call_data, + } + } +} + +impl From for GemYieldData { + fn from(value: primitives::YieldData) -> Self { + GemYieldData { + provider_name: value.provider_name, + contract_address: value.contract_address, + call_data: value.call_data, + } + } +} From 7ed3887db443bd747dee6de9e7f31680c5d3d280 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:06:43 +0900 Subject: [PATCH 12/33] add approval, yield data --- apps/daemon/src/pusher/pusher.rs | 1 + crates/gem_evm/src/provider/preload.rs | 19 +++++++- crates/gem_evm/src/provider/preload_mapper.rs | 45 ++++++++++++++----- crates/primitives/src/lib.rs | 2 +- crates/primitives/src/stake_type.rs | 1 + crates/primitives/src/swap/approval.rs | 3 +- crates/primitives/src/transaction.rs | 8 +++- .../src/transaction_load_metadata.rs | 3 +- crates/primitives/src/transaction_type.rs | 2 + crates/primitives/src/yield_data.rs | 15 +++++++ crates/swapper/src/approval/evm.rs | 4 +- crates/swapper/src/approval/tron.rs | 1 + .../src/thorchain/quote_data_mapper.rs | 1 + crates/yielder/src/models.rs | 3 +- crates/yielder/src/yo/client.rs | 22 +++++++++ crates/yielder/src/yo/provider.rs | 10 +++-- gemstone/src/gem_yielder/mod.rs | 4 +- gemstone/src/gem_yielder/remote_types.rs | 2 + gemstone/src/models/swap.rs | 1 + gemstone/src/models/transaction.rs | 24 +++++++++- 20 files changed, 145 insertions(+), 26 deletions(-) diff --git a/apps/daemon/src/pusher/pusher.rs b/apps/daemon/src/pusher/pusher.rs index 9c3ecb945..462a58900 100644 --- a/apps/daemon/src/pusher/pusher.rs +++ b/apps/daemon/src/pusher/pusher.rs @@ -128,6 +128,7 @@ impl Pusher { title: localizer.notification_unfreeze_title(self.get_value(amount, asset.symbol.clone()).as_str()), message: None, }), + TransactionType::YieldDeposit | TransactionType::YieldWithdraw => Err("Yield transactions not implemented".into()), } } diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index f7452a3e2..9ebd0a0f2 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -17,6 +17,8 @@ use primitives::GasPriceType; #[cfg(feature = "rpc")] use primitives::stake_type::StakeData; #[cfg(feature = "rpc")] +use primitives::yield_data::EvmYieldData; +#[cfg(feature = "rpc")] use primitives::{FeeRate, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput}; #[cfg(feature = "rpc")] use serde_serializers::bigint::bigint_from_hex_str; @@ -66,7 +68,7 @@ impl EthereumClient { let fee = self.calculate_fee(&input, &gas_limit).await?; let metadata = match &input.input_type { - TransactionInputType::Stake(_, _) | TransactionInputType::Yield(_, _, _) => match input.metadata { + TransactionInputType::Stake(_, _) => match input.metadata { TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { nonce, chain_id, @@ -74,6 +76,21 @@ impl EthereumClient { data: if params.data.is_empty() { None } else { Some(hex::encode(¶ms.data)) }, to: Some(params.to), }), + yield_data: None, + }, + _ => input.metadata, + }, + TransactionInputType::Yield(_, _, yield_input) => match input.metadata { + TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data: None, + yield_data: Some(EvmYieldData { + contract_address: yield_input.contract_address.clone(), + call_data: yield_input.call_data.clone(), + approval: yield_input.approval.clone(), + gas_limit: yield_input.gas_limit.clone(), + }), }, _ => input.metadata, }, diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index c8f9f8e43..1aae7d8e1 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::str::FromStr; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, U256, hex}; use alloy_sol_types::SolCall; use gem_bsc::stake_hub::STAKE_HUB_ADDRESS; use num_bigint::BigInt; @@ -38,7 +38,7 @@ pub fn bigint_to_hex_string(value: &BigInt) -> String { } pub fn bytes_to_hex_string(data: &[u8]) -> String { - format!("0x{}", alloy_primitives::hex::encode(data)) + format!("0x{}", hex::encode(data)) } pub fn map_transaction_preload(nonce_hex: String, chain_id: String) -> Result> { @@ -47,6 +47,7 @@ pub fn map_transaction_preload(nonce_hex: String, chain_id: String) -> Result()?, stake_data: None, + yield_data: None, }) } @@ -103,13 +104,13 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> match from_asset.id.token_subtype() { AssetSubtype::NATIVE => Ok(TransactionParams::new( swap_data.data.to.clone(), - alloy_primitives::hex::decode(swap_data.data.data.clone())?, + hex::decode(swap_data.data.data.clone())?, BigInt::from_str_radix(&swap_data.data.value, 10)?, )), AssetSubtype::TOKEN => match swap_data.data.data_type { SwapQuoteDataType::Contract => Ok(TransactionParams::new( swap_data.data.to.clone(), - alloy_primitives::hex::decode(swap_data.data.data.clone())?, + hex::decode(swap_data.data.data.clone())?, BigInt::ZERO, )), SwapQuoteDataType::Transfer => { @@ -161,12 +162,20 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> _ => Err("Unsupported chain for staking".into()), }, TransactionInputType::Yield(_, action, yield_data) => { - let call_data = alloy_primitives::hex::decode(&yield_data.call_data)?; - let tx_value = match action { - YieldAction::Deposit => BigInt::from(0), - YieldAction::Withdraw => BigInt::from(0), - }; - Ok(TransactionParams::new(yield_data.contract_address.clone(), call_data, tx_value)) + if let Some(approval) = &yield_data.approval { + Ok(TransactionParams::new( + approval.token.clone(), + encode_erc20_approve(&approval.spender)?, + BigInt::from(0), + )) + } else { + let call_data = hex::decode(&yield_data.call_data)?; + let tx_value = match action { + YieldAction::Deposit => BigInt::from(0), + YieldAction::Withdraw => BigInt::from(0), + }; + Ok(TransactionParams::new(yield_data.contract_address.clone(), call_data, tx_value)) + } } _ => Err("Unsupported transfer type".into()), } @@ -209,6 +218,14 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { + // When there's an approval, add the yield deposit gas limit + if yield_data.approval.is_some() && yield_data.gas_limit.is_some() { + Ok(BigInt::from_str_radix(yield_data.gas_limit.as_ref().unwrap(), 10)?) + } else { + Ok(BigInt::from(0)) + } + } _ => Ok(BigInt::from(0)), } } @@ -331,10 +348,16 @@ mod tests { let result = map_transaction_preload(nonce_hex, chain_id)?; match result { - TransactionLoadMetadata::Evm { nonce, chain_id, stake_data } => { + TransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data, + yield_data, + } => { assert_eq!(nonce, 10); assert_eq!(chain_id, 1); assert!(stake_data.is_none()); + assert!(yield_data.is_none()); } _ => panic!("Expected Evm variant"), } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index e9437b9ca..c8b12a336 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -230,7 +230,7 @@ pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData pub mod transfer_data_extra; pub use self::transfer_data_extra::TransferDataExtra; pub mod yield_data; -pub use self::yield_data::{YieldAction, YieldData}; +pub use self::yield_data::{EvmYieldData, YieldAction, YieldData}; pub mod transaction_data_output; pub use self::transaction_data_output::{TransferDataOutputAction, TransferDataOutputType}; pub mod broadcast_options; diff --git a/crates/primitives/src/stake_type.rs b/crates/primitives/src/stake_type.rs index 643217fae..1bcf1168a 100644 --- a/crates/primitives/src/stake_type.rs +++ b/crates/primitives/src/stake_type.rs @@ -13,6 +13,7 @@ pub struct RedelegateData { #[derive(Debug, Clone, Serialize, Deserialize)] #[typeshare(swift = "Equatable, Sendable, Hashable")] +#[serde(rename_all = "camelCase")] pub struct StakeData { pub data: Option, pub to: Option, diff --git a/crates/primitives/src/swap/approval.rs b/crates/primitives/src/swap/approval.rs index c4d17038b..3a1a65522 100644 --- a/crates/primitives/src/swap/approval.rs +++ b/crates/primitives/src/swap/approval.rs @@ -3,13 +3,14 @@ use typeshare::typeshare; use crate::{AssetId, Chain, SwapProvider}; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[typeshare(swift = "Equatable, Hashable, Sendable")] #[serde(rename_all = "camelCase")] pub struct ApprovalData { pub token: String, pub spender: String, pub value: String, + pub gas_limit: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/primitives/src/transaction.rs b/crates/primitives/src/transaction.rs index 2d9ac1292..42bb802ec 100644 --- a/crates/primitives/src/transaction.rs +++ b/crates/primitives/src/transaction.rs @@ -287,7 +287,9 @@ impl Transaction { | TransactionType::SmartContractCall | TransactionType::PerpetualOpenPosition | TransactionType::PerpetualClosePosition - | TransactionType::PerpetualModifyPosition => vec![self.asset_id.clone(), self.fee_asset_id.clone()], + | TransactionType::PerpetualModifyPosition + | TransactionType::YieldDeposit + | TransactionType::YieldWithdraw => vec![self.asset_id.clone(), self.fee_asset_id.clone()], TransactionType::Swap => self .metadata .clone() @@ -322,7 +324,9 @@ impl Transaction { | TransactionType::SmartContractCall | TransactionType::PerpetualOpenPosition | TransactionType::PerpetualClosePosition - | TransactionType::PerpetualModifyPosition => vec![AssetAddress::new(self.asset_id.clone(), self.to.clone(), None)], + | TransactionType::PerpetualModifyPosition + | TransactionType::YieldDeposit + | TransactionType::YieldWithdraw => vec![AssetAddress::new(self.asset_id.clone(), self.to.clone(), None)], TransactionType::Swap => self .metadata .clone() diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index d0c19aa04..728b0e845 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,4 +1,4 @@ -use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData}; +use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData, yield_data::EvmYieldData}; use num_bigint::BigInt; use serde::{Deserialize, Serialize}; use serde_serializers::deserialize_bigint_from_str; @@ -59,6 +59,7 @@ pub enum TransactionLoadMetadata { nonce: u64, chain_id: u64, stake_data: Option, + yield_data: Option, }, Near { sequence: u64, diff --git a/crates/primitives/src/transaction_type.rs b/crates/primitives/src/transaction_type.rs index 025d0e20f..c4910eb3b 100644 --- a/crates/primitives/src/transaction_type.rs +++ b/crates/primitives/src/transaction_type.rs @@ -27,6 +27,8 @@ pub enum TransactionType { PerpetualOpenPosition, PerpetualClosePosition, PerpetualModifyPosition, + YieldDeposit, + YieldWithdraw, } impl TransactionType { diff --git a/crates/primitives/src/yield_data.rs b/crates/primitives/src/yield_data.rs index bee024c86..eb5e94e34 100644 --- a/crates/primitives/src/yield_data.rs +++ b/crates/primitives/src/yield_data.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; +use crate::swap::ApprovalData; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[typeshare(swift = "Equatable, Hashable, Sendable")] pub enum YieldAction { @@ -10,8 +12,21 @@ pub enum YieldAction { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[typeshare(swift = "Equatable, Hashable, Sendable")] +#[serde(rename_all = "camelCase")] pub struct YieldData { pub provider_name: String, pub contract_address: String, pub call_data: String, + pub approval: Option, + pub gas_limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Sendable, Hashable")] +#[serde(rename_all = "camelCase")] +pub struct EvmYieldData { + pub contract_address: String, + pub call_data: String, + pub approval: Option, + pub gas_limit: Option, } diff --git a/crates/swapper/src/approval/evm.rs b/crates/swapper/src/approval/evm.rs index f5839608a..e9b81aa5a 100644 --- a/crates/swapper/src/approval/evm.rs +++ b/crates/swapper/src/approval/evm.rs @@ -64,6 +64,7 @@ where token: token.to_string(), spender: spender.to_string(), value: amount.to_string(), + gas_limit: Some("100000".to_string()), })); } Ok(ApprovalType::None) @@ -217,7 +218,8 @@ mod tests { ApprovalType::Approve(ApprovalData { token: token.clone(), spender: permit2_contract.clone(), - value: amount.to_string() + value: amount.to_string(), + gas_limit: Some("100000".to_string()), }), ApprovalType::Permit2(Permit2ApprovalData { token: token.clone(), diff --git a/crates/swapper/src/approval/tron.rs b/crates/swapper/src/approval/tron.rs index 46f858349..3180fc017 100644 --- a/crates/swapper/src/approval/tron.rs +++ b/crates/swapper/src/approval/tron.rs @@ -23,6 +23,7 @@ pub async fn check_approval_tron( token: token_address.to_string(), spender: spender_address.to_string(), value: amount.to_string(), + gas_limit: Some("100000".to_string()), })); } Ok(ApprovalType::None) diff --git a/crates/swapper/src/thorchain/quote_data_mapper.rs b/crates/swapper/src/thorchain/quote_data_mapper.rs index 96a6de11c..6175caf2d 100644 --- a/crates/swapper/src/thorchain/quote_data_mapper.rs +++ b/crates/swapper/src/thorchain/quote_data_mapper.rs @@ -136,6 +136,7 @@ mod tests { token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), spender: "0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146".to_string(), value: "2000".to_string(), + gas_limit: Some("100000".to_string()), }); let result = map_quote_data( diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index c1e75fcdd..088608a00 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -1,7 +1,7 @@ use std::{fmt, str::FromStr}; use alloy_primitives::Address; -use primitives::{AssetId, Chain}; +use primitives::{swap::ApprovalData, AssetId, Chain}; use crate::yo::YieldError; @@ -61,6 +61,7 @@ pub struct YieldTransaction { pub to: String, pub data: String, pub value: Option, + pub approval: Option, } #[derive(Debug, Clone)] diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index c79a8e1d7..5e8bcf487 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -5,6 +5,7 @@ use gem_client::Client; use gem_evm::contracts::IERC20; use gem_evm::multicall3::IMulticall3; use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; +use primitives::swap::ApprovalData; use super::contract::{IYoGateway, IYoVaultToken}; use super::error::YieldError; @@ -33,6 +34,7 @@ pub trait YoProvider: Send + Sync { partner_id: u32, ) -> TransactionObject; async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; + async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YieldError>; } #[derive(Debug, Clone)] @@ -160,4 +162,24 @@ where lookback_timestamp, }) } + + async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YieldError> { + let spender = self.contract_address; + + let mut batch = self.ethereum_client.multicall(); + let allowance_call = batch.add(token, IERC20::allowanceCall { owner, spender }); + let result = batch.execute().await?; + let allowance = result.decode::(&allowance_call)?; + + if allowance < amount { + Ok(Some(ApprovalData { + token: token.to_string(), + spender: spender.to_string(), + value: amount.to_string(), + gas_limit: Some("100000".to_string()), + })) + } else { + Ok(None) + } + } } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 8252af125..ba2d58739 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; -use primitives::{AssetId, Chain}; +use primitives::{swap::ApprovalData, AssetId, Chain}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::provider::YieldProviderClient; @@ -95,8 +95,9 @@ impl YieldProviderClient for YoYieldProvider { let min_shares = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; + let approval = gateway.check_token_allowance(vault.asset_token, wallet, amount).await?; let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, min_shares, receiver, partner_id); - Ok(convert_transaction(vault, tx)) + Ok(convert_transaction(vault, tx, approval)) } async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { @@ -109,7 +110,7 @@ impl YieldProviderClient for YoYieldProvider { let partner_id = YO_PARTNER_ID_GEM; let tx = gateway.build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); - Ok(convert_transaction(vault, tx)) + Ok(convert_transaction(vault, tx, None)) } async fn positions(&self, request: &YieldDetailsRequest) -> Result { @@ -143,13 +144,14 @@ fn parse_value(value: &str) -> Result { U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid value {value}: {err}"))) } -fn convert_transaction(vault: YoVault, tx: TransactionObject) -> YieldTransaction { +fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option) -> YieldTransaction { YieldTransaction { chain: vault.chain, from: tx.from.unwrap_or_default(), to: tx.to, data: tx.data, value: tx.value, + approval, } } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 68c8ad50c..841995f59 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -87,7 +87,7 @@ impl GemYielder { transaction, nonce, chain_id, - gas_limit: "200000".to_string(), + gas_limit: "350000".to_string(), }) } @@ -138,6 +138,8 @@ pub(crate) async fn prepare_yield_input( provider_name: data.provider_name.clone(), contract_address: transaction.to, call_data: transaction.data, + approval: transaction.approval, + gas_limit: Some("350000".to_string()), }, }, sender_address: input.sender_address, diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index d1b021ea0..d75a4a375 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,6 +1,7 @@ use primitives::AssetId; use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; +use crate::models::swap::GemApprovalData; pub use crate::models::transaction::GemYieldAction; pub type GemYieldProvider = YieldProvider; @@ -37,6 +38,7 @@ pub struct GemYieldTransaction { pub to: String, pub data: String, pub value: Option, + pub approval: Option, } pub type GemYieldPosition = YieldPosition; diff --git a/gemstone/src/models/swap.rs b/gemstone/src/models/swap.rs index 3719ce528..827fbe7c6 100644 --- a/gemstone/src/models/swap.rs +++ b/gemstone/src/models/swap.rs @@ -14,6 +14,7 @@ pub struct GemApprovalData { pub token: String, pub spender: String, pub value: String, + pub gas_limit: Option, } #[uniffi::remote(Enum)] diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index cdbf6adb8..e56fe29c8 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -106,6 +106,8 @@ pub enum TransactionType { PerpetualOpenPosition, PerpetualClosePosition, PerpetualModifyPosition, + YieldDeposit, + YieldWithdraw, } pub type GemAccountDataType = AccountDataType; @@ -133,6 +135,16 @@ pub struct GemStakeData { pub to: Option, } +pub type GemEvmYieldData = primitives::EvmYieldData; + +#[uniffi::remote(Record)] +pub struct GemEvmYieldData { + pub contract_address: String, + pub call_data: String, + pub approval: Option, + pub gas_limit: Option, +} + #[uniffi::remote(Record)] pub struct GemHyperliquidOrder { pub approve_agent_required: bool, @@ -266,6 +278,8 @@ pub struct GemYieldData { pub provider_name: String, pub contract_address: String, pub call_data: String, + pub approval: Option, + pub gas_limit: Option, } #[derive(Debug, Clone, uniffi::Enum)] @@ -416,6 +430,7 @@ pub enum GemTransactionLoadMetadata { nonce: u64, chain_id: u64, stake_data: Option, + yield_data: Option, }, Near { sequence: u64, @@ -500,7 +515,7 @@ impl From for GemTransactionLoadMetadata { TransactionLoadMetadata::Bitcoin { utxos } => GemTransactionLoadMetadata::Bitcoin { utxos }, TransactionLoadMetadata::Zcash { utxos, branch_id } => GemTransactionLoadMetadata::Zcash { utxos, branch_id }, TransactionLoadMetadata::Cardano { utxos } => GemTransactionLoadMetadata::Cardano { utxos }, - TransactionLoadMetadata::Evm { nonce, chain_id, stake_data } => GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data }, + TransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data } => GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data }, TransactionLoadMetadata::Near { sequence, block_hash } => GemTransactionLoadMetadata::Near { sequence, block_hash }, TransactionLoadMetadata::Stellar { sequence, @@ -596,7 +611,7 @@ impl From for TransactionLoadMetadata { GemTransactionLoadMetadata::Bitcoin { utxos } => TransactionLoadMetadata::Bitcoin { utxos }, GemTransactionLoadMetadata::Zcash { utxos, branch_id } => TransactionLoadMetadata::Zcash { utxos, branch_id }, GemTransactionLoadMetadata::Cardano { utxos } => TransactionLoadMetadata::Cardano { utxos }, - GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data } => TransactionLoadMetadata::Evm { nonce, chain_id, stake_data }, + GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data } => TransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data }, GemTransactionLoadMetadata::Near { sequence, block_hash } => TransactionLoadMetadata::Near { sequence, block_hash }, GemTransactionLoadMetadata::Stellar { sequence, @@ -872,6 +887,7 @@ impl From for TransactionInputType { token: approval_data.token, spender: approval_data.spender, value: approval_data.value, + gas_limit: approval_data.gas_limit, }, ), GemTransactionInputType::Generic { asset, metadata, extra } => TransactionInputType::Generic(asset, metadata, extra.into()), @@ -925,6 +941,8 @@ impl From for primitives::YieldData { provider_name: value.provider_name, contract_address: value.contract_address, call_data: value.call_data, + approval: value.approval, + gas_limit: value.gas_limit, } } } @@ -935,6 +953,8 @@ impl From for GemYieldData { provider_name: value.provider_name, contract_address: value.contract_address, call_data: value.call_data, + approval: value.approval, + gas_limit: value.gas_limit, } } } From cd529d3494ab99352ddbbc6f0a9f146a454da316 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:29:44 +0900 Subject: [PATCH 13/33] cleanup --- crates/primitives/src/swap/approval.rs | 1 - crates/swapper/src/approval/evm.rs | 2 -- crates/swapper/src/approval/tron.rs | 1 - crates/yielder/src/yo/client.rs | 1 - gemstone/src/gem_yielder/mod.rs | 2 +- gemstone/src/models/swap.rs | 1 - gemstone/src/models/transaction.rs | 1 - 7 files changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/primitives/src/swap/approval.rs b/crates/primitives/src/swap/approval.rs index 3a1a65522..30ee2dc65 100644 --- a/crates/primitives/src/swap/approval.rs +++ b/crates/primitives/src/swap/approval.rs @@ -10,7 +10,6 @@ pub struct ApprovalData { pub token: String, pub spender: String, pub value: String, - pub gas_limit: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/swapper/src/approval/evm.rs b/crates/swapper/src/approval/evm.rs index e9b81aa5a..14092116b 100644 --- a/crates/swapper/src/approval/evm.rs +++ b/crates/swapper/src/approval/evm.rs @@ -64,7 +64,6 @@ where token: token.to_string(), spender: spender.to_string(), value: amount.to_string(), - gas_limit: Some("100000".to_string()), })); } Ok(ApprovalType::None) @@ -219,7 +218,6 @@ mod tests { token: token.clone(), spender: permit2_contract.clone(), value: amount.to_string(), - gas_limit: Some("100000".to_string()), }), ApprovalType::Permit2(Permit2ApprovalData { token: token.clone(), diff --git a/crates/swapper/src/approval/tron.rs b/crates/swapper/src/approval/tron.rs index 3180fc017..46f858349 100644 --- a/crates/swapper/src/approval/tron.rs +++ b/crates/swapper/src/approval/tron.rs @@ -23,7 +23,6 @@ pub async fn check_approval_tron( token: token_address.to_string(), spender: spender_address.to_string(), value: amount.to_string(), - gas_limit: Some("100000".to_string()), })); } Ok(ApprovalType::None) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 5e8bcf487..8d041049b 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -176,7 +176,6 @@ where token: token.to_string(), spender: spender.to_string(), value: amount.to_string(), - gas_limit: Some("100000".to_string()), })) } else { Ok(None) diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 841995f59..88ad8768e 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -87,7 +87,7 @@ impl GemYielder { transaction, nonce, chain_id, - gas_limit: "350000".to_string(), + gas_limit: "300000".to_string(), }) } diff --git a/gemstone/src/models/swap.rs b/gemstone/src/models/swap.rs index 827fbe7c6..3719ce528 100644 --- a/gemstone/src/models/swap.rs +++ b/gemstone/src/models/swap.rs @@ -14,7 +14,6 @@ pub struct GemApprovalData { pub token: String, pub spender: String, pub value: String, - pub gas_limit: Option, } #[uniffi::remote(Enum)] diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index e56fe29c8..add2303f3 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -887,7 +887,6 @@ impl From for TransactionInputType { token: approval_data.token, spender: approval_data.spender, value: approval_data.value, - gas_limit: approval_data.gas_limit, }, ), GemTransactionInputType::Generic { asset, metadata, extra } => TransactionInputType::Generic(asset, metadata, extra.into()), From 8685781b28570474decef83086198cf695dbf802 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:00:36 +0900 Subject: [PATCH 14/33] add convert_to_shares --- crates/yielder/src/yo/client.rs | 9 +++++++++ crates/yielder/src/yo/provider.rs | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 8d041049b..9331abafc 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -35,6 +35,7 @@ pub trait YoProvider: Send + Sync { ) -> TransactionObject; async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YieldError>; + async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result; } #[derive(Debug, Clone)] @@ -181,4 +182,12 @@ where Ok(None) } } + + async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { + let mut batch = self.ethereum_client.multicall(); + let quote_call = batch.add(self.contract_address, IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }); + let result = batch.execute().await?; + let shares = result.decode::("e_call)?; + Ok(shares) + } } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index ba2d58739..0d54e163a 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -105,12 +105,16 @@ impl YieldProviderClient for YoYieldProvider { let gateway = self.gateway_for_chain(vault.chain)?; let wallet = parse_address(wallet_address)?; let receiver = wallet; - let shares = parse_value(value)?; + let assets = parse_value(value)?; let min_assets = U256::from(0); let partner_id = YO_PARTNER_ID_GEM; + // Convert asset amount (e.g., USDC) to shares (e.g., yoUSDC) + let shares = gateway.convert_to_shares(vault.yo_token, assets).await?; + let approval = gateway.check_token_allowance(vault.yo_token, wallet, shares).await?; + let tx = gateway.build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); - Ok(convert_transaction(vault, tx, None)) + Ok(convert_transaction(vault, tx, approval)) } async fn positions(&self, request: &YieldDetailsRequest) -> Result { From 1d670aa5580a623cdf24fc5f9162a8f36fc61838 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:46:06 +0900 Subject: [PATCH 15/33] Update quote_data_mapper.rs --- crates/swapper/src/thorchain/quote_data_mapper.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/swapper/src/thorchain/quote_data_mapper.rs b/crates/swapper/src/thorchain/quote_data_mapper.rs index a02eb298e..bd191d27e 100644 --- a/crates/swapper/src/thorchain/quote_data_mapper.rs +++ b/crates/swapper/src/thorchain/quote_data_mapper.rs @@ -122,7 +122,6 @@ mod tests { token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), spender: "0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146".to_string(), value: "2000".to_string(), - gas_limit: Some("100000".to_string()), }); let result = map_quote_data( From 80cc77664b74309b556815005d2902df2bf0855f Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:07:18 +0900 Subject: [PATCH 16/33] Update preload_mapper.rs --- crates/gem_evm/src/provider/preload_mapper.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index 7c8da6fb5..f52e92756 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -416,8 +416,10 @@ mod tests { assert_eq!(result.len(), 3); - assert_eq!(result[0].gas_price_type.gas_price(), BigInt::ZERO); - assert!(result[0].gas_price_type.priority_fee() != BigInt::ZERO); + // When base_fee is 0, max_fee_per_gas equals priority_fee (0x5f5e100 = 100000000) + let expected_priority_fee = BigInt::from(100000000u64); + assert_eq!(result[0].gas_price_type.gas_price(), expected_priority_fee.clone()); + assert_eq!(result[0].gas_price_type.priority_fee(), expected_priority_fee); Ok(()) } From da52dd3c327b9076fd443156b3dd91942fcd682c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:39:12 +0900 Subject: [PATCH 17/33] Add Yield to banner --- crates/primitives/src/banner.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/primitives/src/banner.rs b/crates/primitives/src/banner.rs index a87453d21..684a7b5d5 100644 --- a/crates/primitives/src/banner.rs +++ b/crates/primitives/src/banner.rs @@ -19,6 +19,7 @@ pub enum BannerEvent { SuspiciousAsset, Onboarding, TradePerpetuals, + Yield, } #[typeshare(swift = "Equatable, CaseIterable, Sendable")] From 238108882c4b3d6e6c843e629d3729e8e7e700e1 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:14:03 +0900 Subject: [PATCH 18/33] fetch performance data --- Cargo.lock | 2 + crates/yielder/Cargo.toml | 3 + crates/yielder/src/lib.rs | 3 +- crates/yielder/src/yo/api/client.rs | 44 ++++++++++++ crates/yielder/src/yo/api/mod.rs | 5 ++ crates/yielder/src/yo/api/model.rs | 29 ++++++++ crates/yielder/src/yo/client.rs | 16 +++-- crates/yielder/src/yo/mod.rs | 2 + crates/yielder/src/yo/provider.rs | 53 ++++++-------- crates/yielder/tests/integration_test.rs | 92 ++++++++++++++---------- gemstone/src/gem_yielder/mod.rs | 6 +- 11 files changed, 178 insertions(+), 77 deletions(-) create mode 100644 crates/yielder/src/yo/api/client.rs create mode 100644 crates/yielder/src/yo/api/mod.rs create mode 100644 crates/yielder/src/yo/api/model.rs diff --git a/Cargo.lock b/Cargo.lock index 3372ed5e8..ab08b53e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9171,7 +9171,9 @@ dependencies = [ "num-traits", "primitives", "reqwest 0.13.1", + "serde", "serde_json", + "serde_serializers", "tokio", ] diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index 16d4bae99..6573cffa9 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -17,9 +17,12 @@ alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } gem_client = { path = "../gem_client" } gem_evm = { path = "../gem_evm", features = ["rpc"] } +gem_jsonrpc = { path = "../gem_jsonrpc" } primitives = { path = "../primitives" } +serde_serializers = { path = "../serde_serializers" } async-trait = { workspace = true } num-traits = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["macros"] } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index e64a25512..f2ce51809 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,5 +5,6 @@ pub mod yo; pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ - IYoGateway, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults, + IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, + YoVault, YoYieldProvider, vaults, }; diff --git a/crates/yielder/src/yo/api/client.rs b/crates/yielder/src/yo/api/client.rs new file mode 100644 index 000000000..86b34fc2c --- /dev/null +++ b/crates/yielder/src/yo/api/client.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use gem_jsonrpc::{RpcProvider, Target}; +use primitives::Chain; + +use super::model::{YoApiResponse, YoPerformanceData}; +use crate::yo::YieldError; + +const YO_API_BASE_URL: &str = "https://api.yo.xyz"; + +pub struct YoApiClient { + rpc_provider: Arc>, +} + +impl YoApiClient { + pub fn new(rpc_provider: Arc>) -> Self { + Self { rpc_provider } + } + + pub async fn fetch_rewards(&self, chain: Chain, vault_address: &str, user_address: &str) -> Result { + let network = match chain { + Chain::Base => "base", + Chain::Ethereum => "mainnet", + _ => return Err(YieldError::new(format!("unsupported chain for Yo API: {:?}", chain))), + }; + let url = format!("{}/api/v1/performance/user/{}/{}/{}", YO_API_BASE_URL, network, vault_address, user_address); + let target = Target::get(&url); + + let response = self + .rpc_provider + .request(target) + .await + .map_err(|e| YieldError::new(format!("API request failed: {}", e)))?; + + let parsed: YoApiResponse = + serde_json::from_slice(&response.data).map_err(|e| YieldError::new(format!("failed to parse Yo API response: {}", e)))?; + + if parsed.status_code != 200 { + return Err(YieldError::new(format!("Yo API error: {}", parsed.message))); + } + + Ok(parsed.data) + } +} diff --git a/crates/yielder/src/yo/api/mod.rs b/crates/yielder/src/yo/api/mod.rs new file mode 100644 index 000000000..eb125d5ad --- /dev/null +++ b/crates/yielder/src/yo/api/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod model; + +pub use client::YoApiClient; +pub use model::YoPerformanceData; diff --git a/crates/yielder/src/yo/api/model.rs b/crates/yielder/src/yo/api/model.rs new file mode 100644 index 000000000..d16618ed6 --- /dev/null +++ b/crates/yielder/src/yo/api/model.rs @@ -0,0 +1,29 @@ +use serde::Deserialize; +use serde_serializers::deserialize_u64_from_str_or_int; + +#[derive(Debug, Deserialize)] +pub struct YoApiResponse { + pub data: T, + pub message: String, + #[serde(rename = "statusCode")] + pub status_code: u32, +} + +#[derive(Debug, Deserialize)] +pub struct YoPerformanceData { + pub realized: YoFormattedValue, + pub unrealized: YoFormattedValue, +} + +#[derive(Debug, Deserialize)] +pub struct YoFormattedValue { + #[serde(deserialize_with = "deserialize_u64_from_str_or_int")] + pub raw: u64, + pub formatted: String, +} + +impl YoPerformanceData { + pub fn total_rewards_raw(&self) -> u64 { + self.realized.raw.saturating_add(self.unrealized.raw) + } +} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 9331abafc..318917c65 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -1,3 +1,4 @@ +use alloy_primitives::hex::{self, encode_prefixed}; use alloy_primitives::{Address, U256}; use alloy_sol_types::SolCall; use async_trait::async_trait; @@ -148,7 +149,6 @@ where let latest_price = latest.decode::(&latest_price_call)?; let latest_timestamp = latest.decode::(&latest_ts)?.to::(); - // Lookback query may fail if vault didn't exist at that block - use latest as fallback let (lookback_price, lookback_timestamp) = self .fetch_lookback_data(vault.yo_token, one_share, multicall_addr, lookback_block) .await @@ -184,10 +184,16 @@ where } async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { - let mut batch = self.ethereum_client.multicall(); - let quote_call = batch.add(self.contract_address, IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }); - let result = batch.execute().await?; - let shares = result.decode::("e_call)?; + let call = IYoGateway::quoteConvertToSharesCall { yoVault: yo_vault, assets }; + let call_data = encode_prefixed(call.abi_encode()); + let result: String = self + .ethereum_client + .eth_call(&self.contract_address.to_string(), &call_data) + .await + .map_err(|e| YieldError::new(format!("eth_call failed: {}", e)))?; + let bytes = hex::decode(&result).map_err(|e| YieldError::new(format!("hex decode failed: {}", e)))?; + let shares = IYoGateway::quoteConvertToSharesCall::abi_decode_returns(&bytes) + .map_err(|e| YieldError::new(format!("abi decode failed: {}", e)))?; Ok(shares) } } diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 446677d73..75dfaad57 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -1,3 +1,4 @@ +mod api; mod client; mod contract; mod error; @@ -5,6 +6,7 @@ mod model; mod provider; mod vault; +pub use api::{YoApiClient, YoPerformanceData}; pub use client::{YoGatewayClient, YoProvider}; pub use contract::{IYoGateway, IYoVaultToken}; pub use error::YieldError; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 0d54e163a..4ad8595a8 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,36 +3,37 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; +use gem_jsonrpc::RpcProvider; use primitives::{swap::ApprovalData, AssetId, Chain}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::provider::YieldProviderClient; +use super::api::YoApiClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; const SECONDS_PER_YEAR: f64 = 31_536_000.0; fn lookback_blocks_for_chain(chain: Chain) -> u64 { match chain { - // Base chain has ~2 second block time, 7 days lookback Chain::Base => 7 * 24 * 60 * 60 / 2, - // Ethereum has ~12 second block time, 7 days lookback Chain::Ethereum => 7 * 24 * 60 * 60 / 12, - _ => 7 * 24 * 60 * 60 / 12, // Default to Ethereum-like + _ => 7 * 24 * 60 * 60 / 12, } } -#[derive(Clone)] -pub struct YoYieldProvider { +pub struct YoYieldProvider { vaults: Vec, gateways: HashMap>, + api_client: YoApiClient, } -impl YoYieldProvider { - pub fn new(gateways: HashMap>) -> Self { +impl YoYieldProvider { + pub fn new(gateways: HashMap>, rpc_provider: Arc>) -> Self { Self { vaults: vaults().to_vec(), gateways, + api_client: YoApiClient::new(rpc_provider), } } @@ -52,7 +53,7 @@ impl YoYieldProvider { } #[async_trait] -impl YieldProviderClient for YoYieldProvider { +impl YieldProviderClient for YoYieldProvider { fn provider(&self) -> YieldProvider { YieldProvider::Yo } @@ -74,7 +75,7 @@ impl YieldProviderClient for YoYieldProvider { async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut results = Vec::new(); - for vault in self.vaults.iter().copied().filter(|vault| vault.asset_id() == *asset_id) { + for vault in self.vaults.iter().copied().filter(|vault: &YoVault| vault.asset_id() == *asset_id) { let gateway = self.gateway_for_chain(vault.chain)?; let lookback_blocks = lookback_blocks_for_chain(vault.chain); let data = gateway.fetch_position_data(vault, Address::ZERO, lookback_blocks).await?; @@ -90,13 +91,10 @@ impl YieldProviderClient for YoYieldProvider { let vault = self.find_vault(asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; let wallet = parse_address(wallet_address)?; - let receiver = wallet; let amount = parse_value(value)?; - let min_shares = U256::from(0); - let partner_id = YO_PARTNER_ID_GEM; let approval = gateway.check_token_allowance(vault.asset_token, wallet, amount).await?; - let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, min_shares, receiver, partner_id); + let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, U256::ZERO, wallet, YO_PARTNER_ID_GEM); Ok(convert_transaction(vault, tx, approval)) } @@ -104,39 +102,34 @@ impl YieldProviderClient for YoYieldProvider { let vault = self.find_vault(asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; let wallet = parse_address(wallet_address)?; - let receiver = wallet; let assets = parse_value(value)?; - let min_assets = U256::from(0); - let partner_id = YO_PARTNER_ID_GEM; - // Convert asset amount (e.g., USDC) to shares (e.g., yoUSDC) let shares = gateway.convert_to_shares(vault.yo_token, assets).await?; let approval = gateway.check_token_allowance(vault.yo_token, wallet, shares).await?; - - let tx = gateway.build_redeem_transaction(wallet, vault.yo_token, shares, min_assets, receiver, partner_id); + let tx = gateway.build_redeem_transaction(wallet, vault.yo_token, shares, U256::ZERO, wallet, YO_PARTNER_ID_GEM); Ok(convert_transaction(vault, tx, approval)) } async fn positions(&self, request: &YieldDetailsRequest) -> Result { let vault = self.find_vault(&request.asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; - let lookback_blocks = lookback_blocks_for_chain(vault.chain); let owner = parse_address(&request.wallet_address)?; - let mut details = YieldPosition::new(vault.name, request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); - - let data = gateway.fetch_position_data(vault, owner, lookback_blocks).await?; + let data = gateway.fetch_position_data(vault, owner, lookback_blocks_for_chain(vault.chain)).await?; - details.vault_balance_value = Some(data.share_balance.to_string()); - - // Calculate asset value from shares: share_balance * latest_price / one_share let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); let asset_value = data.share_balance.saturating_mul(data.latest_price) / one_share; - details.asset_balance_value = Some(asset_value.to_string()); - let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); - details.apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); - Ok(details) + let mut position = YieldPosition::new(vault.name, request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); + position.vault_balance_value = Some(data.share_balance.to_string()); + position.asset_balance_value = Some(asset_value.to_string()); + position.apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); + + if let Ok(performance) = self.api_client.fetch_rewards(vault.chain, &vault.yo_token.to_string(), &request.wallet_address).await { + position.rewards = Some(performance.total_rewards_raw().to_string()); + } + + Ok(position) } } diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index 5748fa3cd..6a48f06f5 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -1,26 +1,29 @@ #![cfg(feature = "yield_integration_tests")] -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; -use alloy_primitives::U256; +use async_trait::async_trait; use gem_client::ReqwestClient; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; -use primitives::EVMChain; +use primitives::{Chain, EVMChain}; use yielder::{ - YO_GATEWAY_BASE_MAINNET, YO_USD, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoYieldProvider, + YO_GATEWAY, YO_USD, YieldDetailsRequest, YieldError, YieldProvider, YieldProviderClient, Yielder, YoApiProvider, YoGatewayClient, YoPerformanceData, + YoProvider, YoYieldProvider, build_performance_url, parse_performance_response, }; fn base_rpc_url() -> String { - std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://mainnet.base.org".to_string()) + std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://gemnodes.com/base".to_string()) } #[tokio::test] async fn test_yields_for_asset_with_apy() -> Result<(), Box> { let jsonrpc_client = JsonRpcClient::new_reqwest(base_rpc_url()); let ethereum_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); - let gateway_client = YoGatewayClient::new(ethereum_client, YO_GATEWAY_BASE_MAINNET); - let provider: Arc = Arc::new(YoYieldProvider::new(Arc::new(gateway_client))); + let gateway_client: Arc = Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY)); + let mut gateways = HashMap::new(); + gateways.insert(Chain::Base, gateway_client); + let provider: Arc = Arc::new(YoYieldProvider::new(gateways)); let yielder = Yielder::with_providers(vec![provider]); let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; @@ -45,13 +48,52 @@ async fn test_yields_for_asset_with_apy() -> Result<(), Box 0, "should have some rewards"); +} + +struct ReqwestYoApiClient; + +#[async_trait] +impl YoApiProvider for ReqwestYoApiClient { + async fn get_user_performance(&self, chain: Chain, vault_address: &str, user_address: &str) -> Result { + let url = build_performance_url(chain, vault_address, user_address)?; + let client = reqwest::Client::new(); + let response = client.get(&url).send().await.map_err(|e| YieldError::new(e.to_string()))?; + let data = response.bytes().await.map_err(|e| YieldError::new(e.to_string()))?; + parse_performance_response(&data) + } +} + +#[tokio::test] +async fn test_yo_positions_with_rewards() { let http_client = ReqwestClient::new_test_client(base_rpc_url()); let jsonrpc_client = JsonRpcClient::new(http_client); let eth_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); - let gateway = Arc::new(YoGatewayClient::base_mainnet(eth_client.clone())); - let gateway_client = YoGatewayClient::base_mainnet(eth_client); - let provider = YoYieldProvider::new(gateway); + let gateway: Arc = Arc::new(YoGatewayClient::new(eth_client, YO_GATEWAY)); + let mut gateways = HashMap::new(); + gateways.insert(Chain::Base, gateway); + + let api_client: Arc = Arc::new(ReqwestYoApiClient); + let provider = YoYieldProvider::new(gateways).with_api_client(api_client); let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; let asset_id = YO_USD.asset_id(); @@ -71,31 +113,7 @@ async fn test_yo_positions() { println!(" Vault Balance (yoUSD shares): {:?}", position.vault_balance_value); println!(" Asset Balance (USDC): {:?}", position.asset_balance_value); println!(" APY: {:?}", position.apy); + println!(" Rewards: {:?}", position.rewards); - let mut total_usd = 0.0; - - if let Some(vault_balance) = &position.vault_balance_value { - let shares: u128 = vault_balance.parse().unwrap_or(0); - let shares_formatted = shares as f64 / 1_000_000.0; - - let shares_u256 = U256::from(shares); - let assets = gateway_client - .quote_convert_to_assets(YO_USD.yo_token, shares_u256) - .await - .expect("should convert shares to assets"); - let assets_value: u128 = assets.to_string().parse().unwrap_or(0); - let assets_usd = assets_value as f64 / 1_000_000.0; - - println!("\n yoUSD shares: {:.6} = ${:.6} USDC", shares_formatted, assets_usd); - total_usd += assets_usd; - } - - if let Some(asset_balance) = &position.asset_balance_value { - let usdc: u128 = asset_balance.parse().unwrap_or(0); - let usdc_formatted = usdc as f64 / 1_000_000.0; - println!(" USDC balance: ${:.6}", usdc_formatted); - total_usd += usdc_formatted; - } - - println!("\n TOTAL USD: ${:.2}", total_usd); + assert!(position.rewards.is_some(), "rewards should be present"); } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 88ad8768e..291dcf1b2 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -12,9 +12,7 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{ - YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider, -}; +use yielder::{YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -108,7 +106,7 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result = Arc::new(YoYieldProvider::new(gateways)); + let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways, wrapper)); let mut yielder = Yielder::new(); yielder.add_provider_arc(yo_provider); Ok(yielder) From e483debc982f4a3fdcb9307bd62c2d4c8cc18ad7 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:26:31 +0900 Subject: [PATCH 19/33] fix fetch_rewards for ethereum --- crates/yielder/src/yo/api/client.rs | 4 ++-- crates/yielder/src/yo/api/model.rs | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/yielder/src/yo/api/client.rs b/crates/yielder/src/yo/api/client.rs index 86b34fc2c..b307664f3 100644 --- a/crates/yielder/src/yo/api/client.rs +++ b/crates/yielder/src/yo/api/client.rs @@ -20,7 +20,7 @@ impl YoApiClient { pub async fn fetch_rewards(&self, chain: Chain, vault_address: &str, user_address: &str) -> Result { let network = match chain { Chain::Base => "base", - Chain::Ethereum => "mainnet", + Chain::Ethereum => "ethereum", _ => return Err(YieldError::new(format!("unsupported chain for Yo API: {:?}", chain))), }; let url = format!("{}/api/v1/performance/user/{}/{}/{}", YO_API_BASE_URL, network, vault_address, user_address); @@ -36,7 +36,7 @@ impl YoApiClient { serde_json::from_slice(&response.data).map_err(|e| YieldError::new(format!("failed to parse Yo API response: {}", e)))?; if parsed.status_code != 200 { - return Err(YieldError::new(format!("Yo API error: {}", parsed.message))); + return Ok(YoPerformanceData::default()); } Ok(parsed.data) diff --git a/crates/yielder/src/yo/api/model.rs b/crates/yielder/src/yo/api/model.rs index d16618ed6..50c525642 100644 --- a/crates/yielder/src/yo/api/model.rs +++ b/crates/yielder/src/yo/api/model.rs @@ -3,22 +3,25 @@ use serde_serializers::deserialize_u64_from_str_or_int; #[derive(Debug, Deserialize)] pub struct YoApiResponse { + #[serde(default)] pub data: T, - pub message: String, #[serde(rename = "statusCode")] pub status_code: u32, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] pub struct YoPerformanceData { + #[serde(default)] pub realized: YoFormattedValue, + #[serde(default)] pub unrealized: YoFormattedValue, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] pub struct YoFormattedValue { - #[serde(deserialize_with = "deserialize_u64_from_str_or_int")] + #[serde(default, deserialize_with = "deserialize_u64_from_str_or_int")] pub raw: u64, + #[serde(default)] pub formatted: String, } From 2b3ba0ec23d1b3cf7f11f27ca24c9122ec17aad4 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:18:14 +0900 Subject: [PATCH 20/33] compile AssetMetaData --- crates/primitives/src/asset_metadata.rs | 38 +++++++++++-------------- crates/primitives/src/lib.rs | 1 + 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/crates/primitives/src/asset_metadata.rs b/crates/primitives/src/asset_metadata.rs index fcc63b3f9..e80871afa 100644 --- a/crates/primitives/src/asset_metadata.rs +++ b/crates/primitives/src/asset_metadata.rs @@ -1,23 +1,19 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + #[typeshare(swift = "Equatable, Hashable, Sendable")] -struct AssetMetaData { - #[serde(rename = "isEnabled")] - is_enabled: bool, - #[serde(rename = "isBalanceEnabled")] - is_balance_enabled: bool, - #[serde(rename = "isBuyEnabled")] - is_buy_enabled: bool, - #[serde(rename = "isSellEnabled")] - is_sell_enabled: bool, - #[serde(rename = "isSwapEnabled")] - is_swap_enabled: bool, - #[serde(rename = "isStakeEnabled")] - is_stake_enabled: bool, - #[serde(rename = "isPinned")] - is_pinned: bool, - #[serde(rename = "isActive")] - is_active: bool, - #[serde(rename = "stakingApr")] - staking_apr: Option, - #[serde(rename = "rankScore")] - rank_score: i32, +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssetMetaData { + pub is_enabled: bool, + pub is_balance_enabled: bool, + pub is_buy_enabled: bool, + pub is_sell_enabled: bool, + pub is_swap_enabled: bool, + pub is_stake_enabled: bool, + pub is_earn_enabled: bool, + pub is_pinned: bool, + pub is_active: bool, + pub staking_apr: Option, + pub rank_score: i32, } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 587069891..36181d7f8 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -59,6 +59,7 @@ pub use self::asset_price_info::AssetPriceInfo; pub mod asset_details; pub use self::asset_details::{AssetBasic, AssetFull, AssetLink, AssetMarketPrice, AssetPriceMetadata, AssetProperties}; pub mod asset_constants; +pub mod asset_metadata; pub mod asset_order; pub use self::asset_order::AssetOrder; pub mod fiat_assets; From 52b3e8e448ba40e152b0bb98b9db896a8c688842 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:08:02 +0900 Subject: [PATCH 21/33] remove is_earn_enabled from metadata --- crates/primitives/src/asset_metadata.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/primitives/src/asset_metadata.rs b/crates/primitives/src/asset_metadata.rs index e80871afa..1c5ac0300 100644 --- a/crates/primitives/src/asset_metadata.rs +++ b/crates/primitives/src/asset_metadata.rs @@ -11,7 +11,6 @@ pub struct AssetMetaData { pub is_sell_enabled: bool, pub is_swap_enabled: bool, pub is_stake_enabled: bool, - pub is_earn_enabled: bool, pub is_pinned: bool, pub is_active: bool, pub staking_apr: Option, From 31a46354d74d4eca7a556f341640c04d14f0ea6a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:42:42 +0900 Subject: [PATCH 22/33] code cleanup reuse reqwest_provider in both swapper and yielder, replace AlienError with gem_client::ClientError --- Cargo.lock | 2 +- bin/gas-bench/Cargo.toml | 1 + bin/gas-bench/src/client.rs | 3 +- bin/gas-bench/src/main.rs | 2 +- crates/gem_aptos/src/rpc/client.rs | 5 +- crates/gem_evm/src/call_decoder.rs | 11 +- crates/gem_evm/src/multicall3.rs | 32 +++-- crates/gem_evm/src/provider/preload.rs | 9 +- crates/gem_evm/src/provider/preload_mapper.rs | 17 +-- crates/gem_jsonrpc/src/lib.rs | 5 + crates/gem_jsonrpc/src/native_provider/mod.rs | 16 +++ .../src/native_provider/reqwest.rs} | 41 +++--- crates/primitives/src/lib.rs | 2 +- .../src/transaction_load_metadata.rs | 6 +- crates/primitives/src/yield_data.rs | 10 -- crates/swapper/Cargo.toml | 7 +- crates/swapper/src/across/provider.rs | 35 +++-- crates/swapper/src/alien/error.rs | 48 ------- crates/swapper/src/alien/mod.rs | 6 +- crates/swapper/src/chainflip/provider.rs | 4 +- crates/swapper/src/client_factory.rs | 4 +- crates/swapper/src/error.rs | 15 +- .../src/hyperliquid/provider/spot/provider.rs | 5 +- crates/swapper/src/jupiter/provider.rs | 3 +- crates/swapper/src/lib.rs | 2 - crates/swapper/src/near_intents/provider.rs | 14 +- crates/swapper/src/proxy/provider.rs | 6 +- crates/swapper/src/swapper.rs | 33 ++++- crates/swapper/src/thorchain/bigint.rs | 58 ++++++++ crates/swapper/src/thorchain/mod.rs | 77 +--------- crates/swapper/src/thorchain/model.rs | 7 +- crates/swapper/src/thorchain/provider.rs | 29 +++- crates/swapper/src/uniswap/v4/provider.rs | 5 +- crates/yielder/src/lib.rs | 4 +- crates/yielder/src/models.rs | 2 +- crates/yielder/src/yo/api/client.rs | 4 +- crates/yielder/src/yo/client.rs | 53 ++----- crates/yielder/src/yo/mod.rs | 2 +- crates/yielder/src/yo/provider.rs | 54 +++---- crates/yielder/tests/integration_test.rs | 135 +++++++----------- gemstone/Cargo.toml | 2 +- gemstone/src/alien/error.rs | 7 +- gemstone/src/alien/reqwest_provider.rs | 4 +- gemstone/src/gateway/error.rs | 14 +- gemstone/src/gateway/mod.rs | 3 +- gemstone/src/gem_yielder/mod.rs | 46 +++--- gemstone/src/models/transaction.rs | 36 +++-- .../com/example/gemtest/NativeProvider.kt | 2 +- .../Extension/Gemstone+Extension.swift | 2 +- .../GemTest/GemTest/Networking/Provider.swift | 2 +- rustfmt.toml | 1 + 51 files changed, 399 insertions(+), 494 deletions(-) create mode 100644 crates/gem_jsonrpc/src/native_provider/mod.rs rename crates/{swapper/src/alien/reqwest_provider.rs => gem_jsonrpc/src/native_provider/reqwest.rs} (69%) delete mode 100644 crates/swapper/src/alien/error.rs create mode 100644 crates/swapper/src/thorchain/bigint.rs diff --git a/Cargo.lock b/Cargo.lock index 03f5df2d3..6d780015d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3137,6 +3137,7 @@ version = "1.0.0" dependencies = [ "clap", "gem_evm", + "gem_jsonrpc", "gemstone", "num-bigint", "prettytable-rs", @@ -7560,7 +7561,6 @@ dependencies = [ "number_formatter", "primitives", "rand 0.9.2", - "reqwest 0.13.1", "serde", "serde_json", "serde_serializers", diff --git a/bin/gas-bench/Cargo.toml b/bin/gas-bench/Cargo.toml index c2eb94502..20bc53b29 100644 --- a/bin/gas-bench/Cargo.toml +++ b/bin/gas-bench/Cargo.toml @@ -13,6 +13,7 @@ serde = { workspace = true, features = ["derive"] } prettytable-rs = "^0.10" primitives = { path = "../../crates/primitives" } +gem_jsonrpc = { path = "../../crates/gem_jsonrpc" } gemstone = { path = "../../gemstone", features = ["reqwest_provider"] } gem_evm = { path = "../../crates/gem_evm" } serde_serializers = { path = "../../crates/serde_serializers" } diff --git a/bin/gas-bench/src/client.rs b/bin/gas-bench/src/client.rs index 0407d3dc5..5e54bdcf9 100644 --- a/bin/gas-bench/src/client.rs +++ b/bin/gas-bench/src/client.rs @@ -3,7 +3,8 @@ use std::error::Error; use gem_evm::fee_calculator::FeeCalculator; use gem_evm::models::fee::EthereumFeeHistory; use gem_evm::{ether_conv::EtherConv, jsonrpc::EthereumRpc}; -use gemstone::alien::{AlienProvider, new_alien_client, reqwest_provider::NativeProvider}; +use gem_jsonrpc::native_provider::NativeProvider; +use gemstone::alien::{AlienProvider, new_alien_client}; use gemstone::network::JsonRpcClient; use num_bigint::BigInt; use primitives::{Chain, PriorityFeeValue, fee::FeePriority}; diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index 9fe1a266a..2985b4ca7 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -13,7 +13,7 @@ use crate::{ etherscan::EtherscanClient, gasflow::GasflowClient, }; -use gemstone::alien::reqwest_provider::NativeProvider; +use gem_jsonrpc::native_provider::NativeProvider; use primitives::fee::FeePriority; #[derive(Debug, Clone)] diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index 5085a8865..651bafabc 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -107,7 +107,10 @@ impl AptosClient { AssetSubtype::TOKEN => Ok(1500), } } - TransactionInputType::Swap(_, _, _) | TransactionInputType::Stake(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) + TransactionInputType::Swap(_, _, _) + | TransactionInputType::Stake(_, _) + | TransactionInputType::TokenApprove(_, _) + | TransactionInputType::Generic(_, _, _) | TransactionInputType::Yield(_, _, _) => Ok(1500), TransactionInputType::Perpetual(_, _) => unimplemented!(), } diff --git a/crates/gem_evm/src/call_decoder.rs b/crates/gem_evm/src/call_decoder.rs index f7097ec05..6783ffebc 100644 --- a/crates/gem_evm/src/call_decoder.rs +++ b/crates/gem_evm/src/call_decoder.rs @@ -22,15 +22,12 @@ pub struct DecodedCall { pub fn decode_call(calldata: &str, abi: Option<&str>) -> Result> { let calldata = hex::decode(calldata)?; - // Check minimum calldata length early if calldata.len() < 4 { return Err("Calldata too short".into()); } - // Try ERC20 interface first if no ABI provided - if abi.is_none() - && let Ok(call) = IERC20Calls::abi_decode(&calldata) - { + let erc20_call = if abi.is_none() { IERC20Calls::abi_decode(&calldata).ok() } else { None }; + if let Some(call) = erc20_call { return Ok(call.into()); } @@ -149,7 +146,6 @@ mod tests { #[test] fn test_decode_custom_abi() { - // Using ERC721 safeTransferFrom as test case let calldata = "0x42842e0e0000000000000000000000008ba1f109551bd432803012645aac136c0c3def25000000000000000000000000271682deb8c4e0901d1a1550ad2e64d568e69909000000000000000000000000000000000000000000000000000000000000007b"; let abi = r#"[ { @@ -191,8 +187,7 @@ mod tests { #[test] fn test_decode_short_calldata() { - // Test that short calldata returns proper error - let result = decode_call("0x1234", None); // Only 2 bytes, need 4 + let result = decode_call("0x1234", None); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Calldata too short")); } diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 913ab1b63..92480ba0a 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -41,10 +41,7 @@ pub struct Multicall3Results { impl Multicall3Results { /// Decode the result for a specific call handle pub fn decode(&self, handle: &CallHandle) -> Result { - let result = self - .results - .get(handle.index) - .ok_or_else(|| Multicall3Error(format!("invalid index: {}", handle.index)))?; + let result = self.results.get(handle.index).ok_or_else(|| Multicall3Error(format!("invalid index: {}", handle.index)))?; if !result.success { return Err(Multicall3Error(format!("{} failed", T::SIGNATURE))); @@ -96,10 +93,7 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { let multicall_address = deployment_by_chain_stack(self.client.chain.chain_stack()); let multicall_data = IMulticall3::aggregate3Call { calls: self.calls }.abi_encode(); - let block_param = self - .block - .map(|n| serde_json::Value::String(format!("0x{n:x}"))) - .unwrap_or_else(|| json!("latest")); + let block_param = self.block.map(|n| serde_json::Value::String(format!("0x{n:x}"))).unwrap_or_else(|| json!("latest")); let result: String = self .client @@ -156,3 +150,25 @@ pub fn decode_call3_return(result: &IMulticall3::Result) -> Result().to_vec().into(), + }], + }; + + let decoded = results.decode::(&handle).expect("decode should succeed"); + assert_eq!(decoded, value); + } +} diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index ac4803556..0ef4ea220 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -17,8 +17,6 @@ use primitives::GasPriceType; #[cfg(feature = "rpc")] use primitives::stake_type::StakeData; #[cfg(feature = "rpc")] -use primitives::yield_data::EvmYieldData; -#[cfg(feature = "rpc")] use primitives::{FeeRate, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput}; #[cfg(feature = "rpc")] use serde_serializers::bigint::bigint_from_hex_str; @@ -83,12 +81,7 @@ impl EthereumClient { nonce, chain_id, stake_data: None, - yield_data: Some(EvmYieldData { - contract_address: yield_input.contract_address.clone(), - call_data: yield_input.call_data.clone(), - approval: yield_input.approval.clone(), - gas_limit: yield_input.gas_limit.clone(), - }), + yield_data: Some(yield_input.clone()), }, _ => input.metadata, }, diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index f52e92756..8537034f1 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -8,8 +8,8 @@ use num_bigint::BigInt; use num_traits::Num; use primitives::swap::SwapQuoteDataType; use primitives::{ - AssetSubtype, Chain, EVMChain, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, YieldAction, - fee::FeePriority, fee::GasPriceType, + AssetSubtype, Chain, EVMChain, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, YieldAction, fee::FeePriority, + fee::GasPriceType, }; use crate::contracts::{IERC20, IERC721, IERC1155}; @@ -100,11 +100,7 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> BigInt::from_str_radix(&swap_data.data.value, 10)?, )), AssetSubtype::TOKEN => match swap_data.data.data_type { - SwapQuoteDataType::Contract => Ok(TransactionParams::new( - swap_data.data.to.clone(), - hex::decode(swap_data.data.data.clone())?, - BigInt::ZERO, - )), + SwapQuoteDataType::Contract => Ok(TransactionParams::new(swap_data.data.to.clone(), hex::decode(swap_data.data.data.clone())?, BigInt::ZERO)), SwapQuoteDataType::Transfer => { let to = from_asset.token_id.clone().ok_or("Missing token ID")?.clone(); let data = encode_erc20_transfer(&swap_data.data.to.clone(), &BigInt::from_str_radix(&input.value, 10)?)?; @@ -151,11 +147,7 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> }, TransactionInputType::Yield(_, action, yield_data) => { if let Some(approval) = &yield_data.approval { - Ok(TransactionParams::new( - approval.token.clone(), - encode_erc20_approve(&approval.spender)?, - BigInt::from(0), - )) + Ok(TransactionParams::new(approval.token.clone(), encode_erc20_approve(&approval.spender)?, BigInt::from(0))) } else { let call_data = hex::decode(&yield_data.call_data)?; let tx_value = match action { @@ -204,7 +196,6 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { - // When there's an approval, add the yield deposit gas limit if yield_data.approval.is_some() && yield_data.gas_limit.is_some() { Ok(BigInt::from_str_radix(yield_data.gas_limit.as_ref().unwrap(), 10)?) } else { diff --git a/crates/gem_jsonrpc/src/lib.rs b/crates/gem_jsonrpc/src/lib.rs index 9791732e1..7f8ceff24 100644 --- a/crates/gem_jsonrpc/src/lib.rs +++ b/crates/gem_jsonrpc/src/lib.rs @@ -9,3 +9,8 @@ pub use client::*; pub mod rpc; #[cfg(feature = "client")] pub use rpc::{HttpMethod, RpcClient, RpcClientError, RpcProvider, RpcResponse, Target}; + +#[cfg(feature = "client")] +pub mod native_provider; +#[cfg(feature = "reqwest")] +pub use native_provider::NativeProvider; diff --git a/crates/gem_jsonrpc/src/native_provider/mod.rs b/crates/gem_jsonrpc/src/native_provider/mod.rs new file mode 100644 index 000000000..4d432042f --- /dev/null +++ b/crates/gem_jsonrpc/src/native_provider/mod.rs @@ -0,0 +1,16 @@ +#[cfg(feature = "client")] +use crate::RpcClientError; +#[cfg(feature = "client")] +use gem_client::ClientError; + +#[cfg(feature = "client")] +impl RpcClientError for ClientError { + fn into_client_error(self) -> ClientError { + self + } +} + +#[cfg(feature = "reqwest")] +pub mod reqwest; +#[cfg(feature = "reqwest")] +pub use reqwest::NativeProvider; diff --git a/crates/swapper/src/alien/reqwest_provider.rs b/crates/gem_jsonrpc/src/native_provider/reqwest.rs similarity index 69% rename from crates/swapper/src/alien/reqwest_provider.rs rename to crates/gem_jsonrpc/src/native_provider/reqwest.rs index 008ca68f0..f2f16c949 100644 --- a/crates/swapper/src/alien/reqwest_provider.rs +++ b/crates/gem_jsonrpc/src/native_provider/reqwest.rs @@ -1,14 +1,12 @@ -use super::{AlienError, HttpMethod, Target}; +use gem_client::ClientError; 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 reqwest::Client; +use crate::{HttpMethod, RpcProvider, RpcResponse, Target}; + #[derive(Debug)] pub struct NativeProvider { - pub client: Client, + client: Client, debug: bool, } @@ -32,14 +30,14 @@ impl Default for NativeProvider { } } -#[async_trait] -impl GenericRpcProvider for NativeProvider { - type Error = AlienError; +#[async_trait::async_trait] +impl RpcProvider for NativeProvider { + type Error = ClientError; fn get_endpoint(&self, chain: Chain) -> Result { let nodes = get_nodes_for_chain(chain); if nodes.is_empty() { - return Err(Self::Error::response_error(format!("not supported chain: {chain:?}"))); + return Err(ClientError::Network(format!("not supported chain: {chain:?}"))); } Ok(nodes[0].url.clone()) } @@ -48,34 +46,38 @@ impl GenericRpcProvider for NativeProvider { if self.debug { println!("==> request: url: {:?}, method: {:?}", target.url, target.method); } - let mut req = match target.method { + + let mut request = match target.method { HttpMethod::Get => self.client.get(target.url), HttpMethod::Post => self.client.post(target.url), HttpMethod::Put => self.client.put(target.url), HttpMethod::Delete => self.client.delete(target.url), HttpMethod::Head => self.client.head(target.url), HttpMethod::Patch => self.client.patch(target.url), - HttpMethod::Options => todo!(), + HttpMethod::Options => return Err(ClientError::Network("options method not supported".to_string())), }; + if let Some(headers) = target.headers { - for (key, value) in headers.iter() { - req = req.header(key, value); + for (key, value) in headers { + request = request.header(&key, value); } } + if let Some(body) = target.body { if self.debug && body.len() <= 4096 { if let Ok(json) = serde_json::from_slice::(&body) { println!("=== json: {json:?}"); } else { - println!("=== body: {:?}", String::from_utf8(body.to_vec()).unwrap()); + println!("=== body: {:?}", String::from_utf8_lossy(&body)); } } - req = req.body(body); + request = request.body(body); } - let response = req.send().map_err(|e| Self::Error::response_error(format!("reqwest send error: {e}"))).await?; + let response = request.send().await.map_err(|e| ClientError::Network(format!("reqwest send error: {e}")))?; let status = response.status(); - let bytes = response.bytes().map_err(|e| Self::Error::response_error(format!("request error: {e}"))).await?; + let bytes = response.bytes().await.map_err(|e| ClientError::Network(format!("request error: {e}")))?; + if self.debug { println!("<== response body size: {:?}", bytes.len()); } @@ -83,9 +85,10 @@ impl GenericRpcProvider for NativeProvider { if let Ok(json) = serde_json::from_slice::(&bytes) { println!("=== json: {json:?}"); } else { - println!("=== body: {:?}", String::from_utf8(bytes.to_vec()).unwrap()); + println!("=== body: {:?}", String::from_utf8_lossy(&bytes)); } } + Ok(RpcResponse { status: Some(status.as_u16()), data: bytes.to_vec(), diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 36181d7f8..897a42ef3 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -228,7 +228,7 @@ pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData pub mod transfer_data_extra; pub use self::transfer_data_extra::TransferDataExtra; pub mod yield_data; -pub use self::yield_data::{EvmYieldData, YieldAction, YieldData}; +pub use self::yield_data::{YieldAction, YieldData}; pub mod transaction_data_output; pub use self::transaction_data_output::{TransferDataOutputAction, TransferDataOutputType}; pub mod broadcast_options; diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index e0bf8af03..80298048b 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,10 +1,8 @@ - - use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData, yield_data::EvmYieldData}; +use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData, yield_data::YieldData}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HyperliquidOrder { @@ -49,7 +47,7 @@ pub enum TransactionLoadMetadata { nonce: u64, chain_id: u64, stake_data: Option, - yield_data: Option, + yield_data: Option, }, Near { sequence: u64, diff --git a/crates/primitives/src/yield_data.rs b/crates/primitives/src/yield_data.rs index eb5e94e34..179240438 100644 --- a/crates/primitives/src/yield_data.rs +++ b/crates/primitives/src/yield_data.rs @@ -20,13 +20,3 @@ pub struct YieldData { pub approval: Option, pub gas_limit: Option, } - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Sendable, Hashable")] -#[serde(rename_all = "camelCase")] -pub struct EvmYieldData { - pub contract_address: String, - pub call_data: String, - pub approval: Option, - pub gas_limit: Option, -} diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index 85575e62c..45e7f1a41 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -6,8 +6,7 @@ license = { workspace = true } [features] default = [] -reqwest_provider = ["dep:reqwest"] -swap_integration_tests = ["reqwest_provider"] +swap_integration_tests = [] [dependencies] primitives = { path = "../primitives" } @@ -18,13 +17,12 @@ gem_evm = { path = "../gem_evm", features = ["rpc"] } gem_sui = { path = "../gem_sui", features = ["rpc"] } gem_aptos = { path = "../gem_aptos", features = ["rpc"] } gem_hash = { path = "../gem_hash" } -gem_jsonrpc = { path = "../gem_jsonrpc" } +gem_jsonrpc = { path = "../gem_jsonrpc", features = ["client"] } gem_client = { path = "../gem_client" } gem_hypercore = { path = "../gem_hypercore" } serde_serializers = { path = "../serde_serializers" } number_formatter = { path = "../number_formatter" } -reqwest = { workspace = true, optional = true } typeshare = { version = "1.0.4" } strum = { workspace = true } @@ -50,3 +48,4 @@ tracing = "0.1.44" [dev-dependencies] tokio.workspace = true +gem_jsonrpc = { path = "../gem_jsonrpc", features = ["reqwest"] } diff --git a/crates/swapper/src/across/provider.rs b/crates/swapper/src/across/provider.rs index 14d6068d5..da95a44e4 100644 --- a/crates/swapper/src/across/provider.rs +++ b/crates/swapper/src/across/provider.rs @@ -604,13 +604,16 @@ mod tests { assert_eq!(fee_in_token.to_string(), "6243790"); } - #[cfg(all(test, feature = "swap_integration_tests", feature = "reqwest_provider"))] + #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; use crate::{ - FetchQuoteData, NativeProvider, Options, QuoteRequest, SwapperError, SwapperMode, + FetchQuoteData, Options, QuoteRequest, SwapperError, SwapperMode, + across::api::DepositStatus, + alien::Target, config::{ReferralFee, ReferralFees}, }; + use gem_jsonrpc::{RpcProvider, native_provider::NativeProvider}; use primitives::{AssetId, Chain, swap::SwapStatus}; use std::{sync::Arc, time::SystemTime}; @@ -650,7 +653,7 @@ mod tests { println!("<== quote: {:?}", quote); assert!(quote.to_value.parse::().unwrap() > 0); - let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::None).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) @@ -688,7 +691,7 @@ mod tests { println!("<== quote: {:?}", quote); assert!(quote.to_value.parse::().unwrap() > 0); - let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::None).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) @@ -699,22 +702,24 @@ mod tests { let network_provider = Arc::new(NativeProvider::default()); let swap_provider = Across::new(network_provider.clone()); - // https://uniscan.xyz/tx/0x9827ca4bdd5dea3a310cff3485f87463987cdc52118077dba34f86ee79456952 - // IMPORTANT: This transaction may not be available on the default Unichain RPC endpoint - // (https://mainnet.unichain.org). It works on https://unichain-rpc.publicnode.com - // The transaction receipt contains: - // - Log 1, Topic 2: deposit ID (0x86f4 = 34548) - let tx_hash = "0x9827ca4bdd5dea3a310cff3485f87463987cdc52118077dba34f86ee79456952"; - let chain = Chain::Unichain; + let chain = Chain::Ethereum; + let deposit_id = "3602896"; + let status_url = format!("https://app.across.to/api/deposit/status?originChainId={}&depositId={}", chain.network_id(), deposit_id); + let target = Target::get(&status_url); + let response = network_provider.request(target).await?; + let status: DepositStatus = serde_json::from_slice(&response.data)?; + let tx_hash = status.deposit_tx_hash.clone(); - let result = swap_provider.get_swap_result(chain, tx_hash).await?; + let result = swap_provider.get_swap_result(chain, &tx_hash).await?; println!("Across swap result: {:?}", result); assert_eq!(result.from_chain, chain); assert_eq!(result.from_tx_hash, tx_hash); - assert_eq!(result.status, SwapStatus::Completed); - assert_eq!(result.to_chain, Some(Chain::Linea)); - assert_eq!(result.to_tx_hash, Some("0xcba653515ab00f5b3ebc16eb4d099e29611e1e59b3fd8f2800cf2302d175f9fe".to_string())); + assert_eq!(result.status, status.swap_status()); + assert_eq!(result.to_chain, Chain::from_chain_id(status.destination_chain_id)); + if result.status == SwapStatus::Completed { + assert_eq!(result.to_tx_hash, status.fill_tx); + } Ok(()) } 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/mod.rs b/crates/swapper/src/alien/mod.rs index 9ceaab23b..393c0a045 100644 --- a/crates/swapper/src/alien/mod.rs +++ b/crates/swapper/src/alien/mod.rs @@ -1,11 +1,7 @@ -pub mod error; pub mod mock; -#[cfg(feature = "reqwest_provider")] -pub mod reqwest_provider; -pub use error::AlienError; +pub use gem_client::ClientError as AlienError; pub use gem_jsonrpc::{HttpMethod, RpcClient as GenericRpcClient, RpcProvider as GenericRpcProvider, Target}; - pub type RpcClient = GenericRpcClient; pub trait RpcProvider: GenericRpcProvider {} diff --git a/crates/swapper/src/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs index f1ecfaf36..e4740fcca 100644 --- a/crates/swapper/src/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -17,10 +17,10 @@ use super::{ use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteData, alien::RpcProvider, + amount_to_value, approval::check_approval_erc20, asset::{ARBITRUM_USDC, ETHEREUM_FLIP, ETHEREUM_USDC, ETHEREUM_USDT, SOLANA_USDC}, config::DEFAULT_CHAINFLIP_FEE_BPS, - amount_to_value, slippage, }; use primitives::{ChainType, chain::Chain, swap::QuoteAsset}; @@ -387,7 +387,7 @@ mod tests { #[tokio::test] #[cfg(feature = "swap_integration_tests")] async fn test_get_swap_result() -> Result<(), Box> { - use crate::alien::reqwest_provider::NativeProvider; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::swap::SwapStatus; let network_provider = Arc::new(NativeProvider::default()); diff --git a/crates/swapper/src/client_factory.rs b/crates/swapper/src/client_factory.rs index d9e2f4f0d..04cacfc6f 100644 --- a/crates/swapper/src/client_factory.rs +++ b/crates/swapper/src/client_factory.rs @@ -29,10 +29,10 @@ pub fn create_eth_client(provider: Arc, chain: Chain) -> Result Ok(EthereumClient::new(client, evm_chain)) } -#[cfg(all(test, feature = "reqwest_provider", feature = "swap_integration_tests"))] +#[cfg(all(test, feature = "swap_integration_tests"))] mod tests { use super::*; - use crate::NativeProvider; + use gem_jsonrpc::native_provider::NativeProvider; use gem_solana::{jsonrpc::SolanaRpc, models::blockhash::SolanaBlockhashResult}; use std::sync::Arc; diff --git a/crates/swapper/src/error.rs b/crates/swapper/src/error.rs index e68cea019..a00e866d7 100644 --- a/crates/swapper/src/error.rs +++ b/crates/swapper/src/error.rs @@ -1,4 +1,3 @@ -use crate::alien::AlienError; use crate::proxy::ProxyError; use crate::thorchain::model::ErrorResponse as ThorchainError; use gem_client::ClientError; @@ -47,16 +46,6 @@ impl std::fmt::Display for SwapperError { impl std::error::Error for SwapperError {} -impl From for SwapperError { - fn from(err: AlienError) -> Self { - match err { - AlienError::RequestError { msg } => Self::ComputeQuoteError(msg), - AlienError::ResponseError { msg } => Self::ComputeQuoteError(msg), - AlienError::Http { status, .. } => Self::ComputeQuoteError(format!("HTTP error: status {}", status)), - } - } -} - impl From for SwapperError { fn from(err: JsonRpcError) -> Self { Self::ComputeQuoteError(format!("JSON RPC error: {err}")) @@ -75,7 +64,9 @@ impl From for SwapperError { if let Ok(thorchain_error) = serde_json::from_slice::(body) && thorchain_error.is_input_amount_error() { - return Self::InputAmountError { min_amount: thorchain_error.parse_min_amount() }; + return Self::InputAmountError { + min_amount: thorchain_error.parse_min_amount(), + }; } Self::ComputeQuoteError(format!("HTTP error: status {}", status)) } diff --git a/crates/swapper/src/hyperliquid/provider/spot/provider.rs b/crates/swapper/src/hyperliquid/provider/spot/provider.rs index 6014affc6..92aee632c 100644 --- a/crates/swapper/src/hyperliquid/provider/spot/provider.rs +++ b/crates/swapper/src/hyperliquid/provider/spot/provider.rs @@ -252,10 +252,11 @@ impl Swapper for HyperCoreSpot { } } -#[cfg(all(test, feature = "swap_integration_tests", feature = "reqwest_provider"))] +#[cfg(all(test, feature = "swap_integration_tests"))] mod tests { use super::*; use crate::{hyperliquid::provider::spot::math::SPOT_ASSET_OFFSET, testkit::mock_quote}; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::swap::SwapQuoteDataType; use std::str::FromStr; @@ -268,7 +269,7 @@ mod tests { } async fn assert_spot_quote(from_asset: SwapperQuoteAsset, to_asset: SwapperQuoteAsset) { - let spot = HyperCoreSpot::new(Arc::new(crate::NativeProvider::new())); + let spot = HyperCoreSpot::new(Arc::new(NativeProvider::new())); let mut request = mock_quote(from_asset, to_asset); request.options.preferred_providers = vec![SwapperProvider::Hyperliquid]; diff --git a/crates/swapper/src/jupiter/provider.rs b/crates/swapper/src/jupiter/provider.rs index f3bcb0394..1bb4efe04 100644 --- a/crates/swapper/src/jupiter/provider.rs +++ b/crates/swapper/src/jupiter/provider.rs @@ -212,7 +212,8 @@ where #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{FetchQuoteData, SwapperMode, SwapperQuoteAsset, alien::reqwest_provider::NativeProvider, models::Options}; + use crate::{FetchQuoteData, SwapperMode, SwapperQuoteAsset, models::Options}; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::AssetId; use std::sync::Arc; diff --git a/crates/swapper/src/lib.rs b/crates/swapper/src/lib.rs index d20e161f4..c5ff0b6e4 100644 --- a/crates/swapper/src/lib.rs +++ b/crates/swapper/src/lib.rs @@ -39,8 +39,6 @@ pub fn amount_to_value(token: &str, decimals: u32) -> Option { } } -#[cfg(feature = "reqwest_provider")] -pub use alien::reqwest_provider::NativeProvider; pub use alien::{AlienError, HttpMethod, RpcClient, RpcProvider, Target}; pub use error::SwapperError; pub use models::*; diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index e090fd362..a74754e2a 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -6,7 +6,7 @@ use super::{ }; use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperMode, - SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, client_factory::create_client_with_chain, amount_to_value, near_intents::client::base_url, + SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, amount_to_value, client_factory::create_client_with_chain, near_intents::client::base_url, }; use alloy_primitives::U256; use async_trait::async_trait; @@ -466,10 +466,11 @@ mod tests { } } -#[cfg(all(test, feature = "swap_integration_tests", feature = "reqwest_provider"))] +#[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{FetchQuoteData, SwapperMode, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, alien::reqwest_provider::NativeProvider, models::Options}; + use crate::{FetchQuoteData, SwapperMode, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, models::Options}; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::{AssetId, Chain, swap::SwapStatus}; use std::sync::Arc; @@ -525,7 +526,7 @@ mod swap_integration_tests { to_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Near)), wallet_address: "GBZXN7PIRZGNMHGA3RSSOEV56YXG54FSNTJDGQI3GHDVBKSXRZ5B6KJT".to_string(), destination_address: "test.near".to_string(), - value: "1000000".to_string(), + value: "12000000".to_string(), mode: SwapperMode::ExactIn, options, }; @@ -541,7 +542,10 @@ mod swap_integration_tests { Err(error) => return Err(error), }; - assert!(!quote_data.data.is_empty(), "expected deposit memo for Stellar swaps via Near Intents"); + assert!( + quote_data.memo.as_ref().is_some_and(|memo| !memo.is_empty()), + "expected deposit memo for Stellar swaps via Near Intents" + ); Ok(()) } diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 8da3e5f3f..cedba8b98 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -260,10 +260,8 @@ where #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{ - alien::reqwest_provider::NativeProvider, - {SwapperMode, SwapperQuoteAsset, asset::SUI_USDC_TOKEN_ID, models::Options}, - }; + use crate::{SwapperMode, SwapperQuoteAsset, asset::SUI_USDC_TOKEN_ID, models::Options}; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::AssetId; #[tokio::test] diff --git a/crates/swapper/src/swapper.rs b/crates/swapper/src/swapper.rs index 42b7477fb..5821470aa 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -217,7 +217,7 @@ impl GemSwapper { } } -#[cfg(all(test, feature = "reqwest_provider"))] +#[cfg(test)] mod tests { use std::{borrow::Cow, collections::BTreeSet, sync::Arc, vec}; @@ -227,11 +227,11 @@ mod tests { use super::*; use crate::{ Options, SwapperChainAsset, SwapperMode, SwapperProvider, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, - alien::reqwest_provider::NativeProvider, config::{DEFAULT_STABLE_SWAP_REFERRAL_BPS, DEFAULT_SWAP_FEE_BPS, ReferralFees}, testkit::{MockSwapper, mock_quote}, uniswap::default::{new_pancakeswap, new_uniswap_v3}, }; + use gem_jsonrpc::native_provider::NativeProvider; fn build_request(from_symbol: &str, to_symbol: &str, fee: Option) -> QuoteRequest { QuoteRequest { @@ -410,7 +410,9 @@ mod tests { rpc_provider: Arc::new(NativeProvider::default()), swappers: vec![ Box::new(MockSwapper::new(SwapperProvider::UniswapV3, || Err(SwapperError::InputAmountError { min_amount: None }))), - Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || Err(SwapperError::InputAmountError { min_amount: None }))), + Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || { + Err(SwapperError::InputAmountError { min_amount: None }) + })), Box::new(MockSwapper::new(SwapperProvider::Jupiter, || Err(SwapperError::NoQuoteAvailable))), ], }; @@ -419,11 +421,28 @@ mod tests { let gem_swapper = GemSwapper { rpc_provider: Arc::new(NativeProvider::default()), swappers: vec![ - Box::new(MockSwapper::new(SwapperProvider::UniswapV3, || Err(SwapperError::InputAmountError { min_amount: Some("19630000".into()) }))), - Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || Err(SwapperError::InputAmountError { min_amount: Some("1264000".into()) }))), - Box::new(MockSwapper::new(SwapperProvider::Jupiter, || Err(SwapperError::InputAmountError { min_amount: Some("68000000".into()) }))), + Box::new(MockSwapper::new(SwapperProvider::UniswapV3, || { + Err(SwapperError::InputAmountError { + min_amount: Some("19630000".into()), + }) + })), + Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || { + Err(SwapperError::InputAmountError { + min_amount: Some("1264000".into()), + }) + })), + Box::new(MockSwapper::new(SwapperProvider::Jupiter, || { + Err(SwapperError::InputAmountError { + min_amount: Some("68000000".into()), + }) + })), ], }; - assert_eq!(gem_swapper.fetch_quote(&request).await, Err(SwapperError::InputAmountError { min_amount: Some("1264000".into()) })); + assert_eq!( + gem_swapper.fetch_quote(&request).await, + Err(SwapperError::InputAmountError { + min_amount: Some("1264000".into()) + }) + ); } } diff --git a/crates/swapper/src/thorchain/bigint.rs b/crates/swapper/src/thorchain/bigint.rs new file mode 100644 index 000000000..a4ca7ef4a --- /dev/null +++ b/crates/swapper/src/thorchain/bigint.rs @@ -0,0 +1,58 @@ +use num_bigint::BigInt; + +use crate::SwapperError; + +const THORCHAIN_BASE_DECIMALS: i32 = 8; + +pub(crate) fn value_from(value: &str, decimals: i32) -> Result { + let value = value.parse::()?; + let decimals = decimals - THORCHAIN_BASE_DECIMALS; + let factor = BigInt::from(10).pow(decimals.unsigned_abs()); + Ok(if decimals > 0 { value / factor } else { value * factor }) +} + +pub(crate) fn value_to(value: &str, decimals: i32) -> Result { + let value = value.parse::()?; + let decimals = decimals - THORCHAIN_BASE_DECIMALS; + let factor = BigInt::from(10).pow(decimals.unsigned_abs()); + Ok(if decimals > 0 { value * factor } else { value / factor }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_value_from() { + let value = "1000000000"; + + let result = value_from(value, 18).unwrap(); + assert_eq!(result, BigInt::from(0)); + + let result = value_from(value, 10).unwrap(); + assert_eq!(result, BigInt::from(10000000)); + + let result = value_from(value, 6).unwrap(); + assert_eq!(result, BigInt::from(100000000000u64)); + + let result = value_from(value, 8).unwrap(); + assert_eq!(result, BigInt::from(1000000000u64)); + } + + #[test] + fn test_value_to() { + let value = "10000000"; + + let result = value_to(value, 18).unwrap(); + assert_eq!(result, BigInt::from(100000000000000000u64)); + + let result = value_to(value, 10).unwrap(); + assert_eq!(result, BigInt::from(1000000000u64)); + + let result = value_to(value, 6).unwrap(); + assert_eq!(result, BigInt::from(100000u64)); + + let result = value_to(value, 8).unwrap(); + assert_eq!(result, BigInt::from(10000000u64)); + } +} diff --git a/crates/swapper/src/thorchain/mod.rs b/crates/swapper/src/thorchain/mod.rs index ab93ee2aa..55abf34ea 100644 --- a/crates/swapper/src/thorchain/mod.rs +++ b/crates/swapper/src/thorchain/mod.rs @@ -1,4 +1,5 @@ mod asset; +mod bigint; mod chain; mod client; mod constants; @@ -7,9 +8,8 @@ pub(crate) mod model; mod provider; mod quote_data_mapper; -use num_bigint::BigInt; use primitives::Chain; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use crate::alien::RpcProvider; use gem_client::Client; @@ -46,80 +46,7 @@ where } } - fn value_from(&self, value: String, decimals: i32) -> BigInt { - let value = BigInt::from_str(&value).unwrap(); - let decimals = decimals - 8; - let factor = BigInt::from(10).pow(decimals.unsigned_abs()); - if decimals > 0 { value / factor } else { value * factor } - } - - fn value_to(&self, value: String, decimals: i32) -> BigInt { - let value = BigInt::from_str(&value).unwrap(); - let decimals = decimals - 8; - let factor = BigInt::from(10).pow(decimals.unsigned_abs()); - if decimals > 0 { value * factor } else { value / factor } - } - fn get_eta_in_seconds(&self, destination_chain: Chain, total_swap_seconds: Option) -> u32 { destination_chain.block_time() / 1000 + OUTBOUND_DELAY_SECONDS + total_swap_seconds.unwrap_or(0) } } - -#[cfg(all(test, feature = "reqwest_provider"))] -mod tests { - use super::*; - use crate::alien::reqwest_provider::NativeProvider; - use std::sync::Arc; - - #[test] - fn test_value_from() { - let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); - - let value = "1000000000".to_string(); - - let result = thorchain.value_from(value.clone(), 18); - assert_eq!(result, BigInt::from_str("0").unwrap()); - - let result = thorchain.value_from(value.clone(), 10); - assert_eq!(result, BigInt::from_str("10000000").unwrap()); - - let result = thorchain.value_from(value.clone(), 6); - assert_eq!(result, BigInt::from_str("100000000000").unwrap()); - - let result = thorchain.value_from(value.clone(), 8); - assert_eq!(result, BigInt::from(1000000000)); - } - - #[test] - fn test_value_to() { - let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); - - let value = "10000000".to_string(); - - let result = thorchain.value_to(value.clone(), 18); - assert_eq!(result, BigInt::from_str("100000000000000000").unwrap()); - - let result = thorchain.value_to(value.clone(), 10); - assert_eq!(result, BigInt::from(1000000000)); - - let result = thorchain.value_to(value.clone(), 6); - assert_eq!(result, BigInt::from(100000)); - - let result = thorchain.value_to(value.clone(), 8); - assert_eq!(result, BigInt::from(10000000)); - } - - #[test] - fn test_get_eta_in_seconds() { - let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); - - let eta = thorchain.get_eta_in_seconds(Chain::Bitcoin, None); - assert_eq!(eta, 660); - - let eta = thorchain.get_eta_in_seconds(Chain::Bitcoin, Some(1200)); - assert_eq!(eta, 1860); - - let eta = thorchain.get_eta_in_seconds(Chain::SmartChain, Some(648)); - assert_eq!(eta, 709); - } -} diff --git a/crates/swapper/src/thorchain/model.rs b/crates/swapper/src/thorchain/model.rs index b03ae1028..031ebd344 100644 --- a/crates/swapper/src/thorchain/model.rs +++ b/crates/swapper/src/thorchain/model.rs @@ -95,12 +95,7 @@ impl ErrorResponse { pub fn parse_min_amount(&self) -> Option { self.message .find(Self::MIN_AMOUNT_PREFIX) - .map(|start| { - self.message[start + Self::MIN_AMOUNT_PREFIX.len()..] - .chars() - .take_while(|c| c.is_ascii_digit()) - .collect() - }) + .map(|start| self.message[start + Self::MIN_AMOUNT_PREFIX.len()..].chars().take_while(|c| c.is_ascii_digit()).collect()) .filter(|s: &String| !s.is_empty()) } } diff --git a/crates/swapper/src/thorchain/provider.rs b/crates/swapper/src/thorchain/provider.rs index 09bf35e64..2226b797d 100644 --- a/crates/swapper/src/thorchain/provider.rs +++ b/crates/swapper/src/thorchain/provider.rs @@ -5,7 +5,9 @@ use async_trait::async_trait; use gem_client::Client; use primitives::{Chain, swap::ApprovalData}; -use super::{QUOTE_INTERVAL, QUOTE_MINIMUM, QUOTE_QUANTITY, ThorChain, asset::THORChainAsset, chain::THORChainName, memo::ThorchainMemo, model::RouteData, quote_data_mapper}; +use super::{ + QUOTE_INTERVAL, QUOTE_MINIMUM, QUOTE_QUANTITY, ThorChain, asset::THORChainAsset, bigint, chain::THORChainName, memo::ThorchainMemo, model::RouteData, quote_data_mapper, +}; use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperQuoteData, approval::check_approval_erc20, asset::*, thorchain::client::ThorChainSwapClient, @@ -55,7 +57,7 @@ where let from_asset = THORChainAsset::from_asset_id(&request.from_asset.id).ok_or(SwapperError::NotSupportedAsset)?; let to_asset = THORChainAsset::from_asset_id(&request.to_asset.id).ok_or(SwapperError::NotSupportedAsset)?; - let value = self.value_from(request.clone().value, from_asset.decimals as i32); + let value = bigint::value_from(&request.value, from_asset.decimals as i32)?; if from_asset.chain != THORChainName::Thorchain { let inbound_addresses = self.swap_client.get_inbound_addresses().await?; @@ -84,7 +86,7 @@ where ) .await?; - let to_value = self.value_to(quote.expected_amount_out, to_asset.decimals as i32); + let to_value = bigint::value_to("e.expected_amount_out, to_asset.decimals as i32)?; let inbound_address = RouteData::get_inbound_address(&from_asset, quote.inbound_address.clone())?; let route_data = RouteData { router_address: quote.router.clone(), @@ -167,9 +169,7 @@ where .as_ref() .and_then(|hashes| hashes.iter().find(|h| *h != ZERO_HASH && !h.is_empty()).cloned()); - let (to_chain, to_tx_hash) = destination_chain - .map(|chain| (Some(chain), destination_tx_hash)) - .unwrap_or((None, None)); + let (to_chain, to_tx_hash) = destination_chain.map(|chain| (Some(chain), destination_tx_hash)).unwrap_or((None, None)); Ok(SwapResult { status: swap_status, @@ -184,7 +184,8 @@ where #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{SwapperQuoteAsset, alien::reqwest_provider::NativeProvider, testkit::mock_quote}; + use crate::{SwapperQuoteAsset, testkit::mock_quote}; + use gem_jsonrpc::native_provider::NativeProvider; use std::sync::Arc; #[tokio::test] @@ -264,4 +265,18 @@ mod swap_integration_tests { Ok(()) } + + #[test] + fn test_get_eta_in_seconds() { + let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); + + let eta = thorchain.get_eta_in_seconds(Chain::Bitcoin, None); + assert_eq!(eta, 660); + + let eta = thorchain.get_eta_in_seconds(Chain::Bitcoin, Some(1200)); + assert_eq!(eta, 1860); + + let eta = thorchain.get_eta_in_seconds(Chain::SmartChain, Some(648)); + assert_eq!(eta, 709); + } } diff --git a/crates/swapper/src/uniswap/v4/provider.rs b/crates/swapper/src/uniswap/v4/provider.rs index 3f5d418ee..18e5037be 100644 --- a/crates/swapper/src/uniswap/v4/provider.rs +++ b/crates/swapper/src/uniswap/v4/provider.rs @@ -295,14 +295,15 @@ mod tests { assert_eq!(swapper.provider.id, SwapperProvider::UniswapV4); } - #[cfg(all(test, feature = "swap_integration_tests", feature = "reqwest_provider"))] + #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; use crate::{ - FetchQuoteData, NativeProvider, Options, QuoteRequest, SwapperError, SwapperMode, SwapperProvider, + FetchQuoteData, Options, QuoteRequest, SwapperError, SwapperMode, SwapperProvider, config::{ReferralFee, ReferralFees}, uniswap, }; + use gem_jsonrpc::native_provider::NativeProvider; use primitives::{AssetId, Chain}; use std::{sync::Arc, time::SystemTime}; diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index f2ce51809..8aed2ef74 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,6 +5,6 @@ pub mod yo; pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ - IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, - YoVault, YoYieldProvider, vaults, + IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, YoVault, YoYieldProvider, + vaults, }; diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index 088608a00..9be7552b1 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -1,7 +1,7 @@ use std::{fmt, str::FromStr}; use alloy_primitives::Address; -use primitives::{swap::ApprovalData, AssetId, Chain}; +use primitives::{AssetId, Chain, swap::ApprovalData}; use crate::yo::YieldError; diff --git a/crates/yielder/src/yo/api/client.rs b/crates/yielder/src/yo/api/client.rs index b307664f3..0148b8f75 100644 --- a/crates/yielder/src/yo/api/client.rs +++ b/crates/yielder/src/yo/api/client.rs @@ -30,10 +30,10 @@ impl YoApiClient { .rpc_provider .request(target) .await - .map_err(|e| YieldError::new(format!("API request failed: {}", e)))?; + .map_err(|e| YieldError::new(format!("fetch performance error: request failed: {e}")))?; let parsed: YoApiResponse = - serde_json::from_slice(&response.data).map_err(|e| YieldError::new(format!("failed to parse Yo API response: {}", e)))?; + serde_json::from_slice(&response.data).map_err(|e| YieldError::new(format!("fetch performance error: failed to parse response: {e}")))?; if parsed.status_code != 200 { return Ok(YoPerformanceData::default()); diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 318917c65..3ed7e2d1c 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -8,32 +8,16 @@ use gem_evm::multicall3::IMulticall3; use gem_evm::{jsonrpc::TransactionObject, rpc::EthereumClient}; use primitives::swap::ApprovalData; +use super::YoVault; use super::contract::{IYoGateway, IYoVaultToken}; use super::error::YieldError; use super::model::PositionData; -use super::YoVault; #[async_trait] pub trait YoProvider: Send + Sync { fn contract_address(&self) -> Address; - fn build_deposit_transaction( - &self, - from: Address, - yo_vault: Address, - assets: U256, - min_shares_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject; - fn build_redeem_transaction( - &self, - from: Address, - yo_vault: Address, - shares: U256, - min_assets_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject; + fn build_deposit_transaction(&self, from: Address, yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; + fn build_redeem_transaction(&self, from: Address, yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YieldError>; async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result; @@ -97,28 +81,12 @@ where self.contract_address } - fn build_deposit_transaction( - &self, - from: Address, - yo_vault: Address, - assets: U256, - min_shares_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject { + fn build_deposit_transaction(&self, from: Address, yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { let data = Self::deposit_call_data(yo_vault, 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_vault: Address, - shares: U256, - min_assets_out: U256, - receiver: Address, - partner_id: u32, - ) -> TransactionObject { + fn build_redeem_transaction(&self, from: Address, yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { let data = Self::redeem_call_data(yo_vault, shares, min_assets_out, receiver, partner_id); TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } @@ -132,9 +100,7 @@ where let lookback_block = latest_block.saturating_sub(lookback_blocks); let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); - let multicall_addr: Address = gem_evm::multicall3::deployment_by_chain_stack(self.ethereum_client.chain.chain_stack()) - .parse() - .unwrap(); + let multicall_addr: Address = gem_evm::multicall3::deployment_by_chain_stack(self.ethereum_client.chain.chain_stack()).parse().unwrap(); let mut latest_batch = self.ethereum_client.multicall(); let share_bal = latest_batch.add(vault.yo_token, IERC20::balanceOfCall { account: owner }); @@ -190,10 +156,9 @@ where .ethereum_client .eth_call(&self.contract_address.to_string(), &call_data) .await - .map_err(|e| YieldError::new(format!("eth_call failed: {}", e)))?; - let bytes = hex::decode(&result).map_err(|e| YieldError::new(format!("hex decode failed: {}", e)))?; - let shares = IYoGateway::quoteConvertToSharesCall::abi_decode_returns(&bytes) - .map_err(|e| YieldError::new(format!("abi decode failed: {}", e)))?; + .map_err(|e| YieldError::new(format!("convert_to_shares eth_call failed: {e}")))?; + let bytes = hex::decode(&result).map_err(|e| YieldError::new(format!("convert_to_shares hex decode failed: {e}")))?; + let shares = IYoGateway::quoteConvertToSharesCall::abi_decode_returns(&bytes).map_err(|e| YieldError::new(format!("convert_to_shares abi decode failed: {e}")))?; Ok(shares) } } diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 75dfaad57..d879652da 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -14,7 +14,7 @@ pub use model::PositionData; pub use provider::YoYieldProvider; pub use vault::{YO_USD, YO_USDT, YoVault, vaults}; -use alloy_primitives::{address, Address}; +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 index 4ad8595a8..3f083c452 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -4,7 +4,7 @@ use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; use gem_jsonrpc::RpcProvider; -use primitives::{swap::ApprovalData, AssetId, Chain}; +use primitives::{AssetId, Chain, swap::ApprovalData}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::provider::YieldProviderClient; @@ -38,18 +38,27 @@ impl YoYieldProvider { } fn find_vault(&self, asset_id: &AssetId) -> Result { - self.vaults - .iter() - .copied() - .find(|vault| vault.asset_id() == *asset_id) + self.vaults_for_asset(asset_id) + .next() .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) } + fn vaults_for_asset(&self, asset_id: &AssetId) -> impl Iterator + '_ { + let asset_id = asset_id.clone(); + self.vaults.iter().copied().filter(move |vault| vault.asset_id() == asset_id) + } + fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YieldError> { self.gateways .get(&chain) .ok_or_else(|| YieldError::new(format!("no gateway configured for chain {:?}", chain))) } + + fn vault_and_gateway(&self, asset_id: &AssetId) -> Result<(YoVault, &Arc), YieldError> { + let vault = self.find_vault(asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; + Ok((vault, gateway)) + } } #[async_trait] @@ -59,23 +68,15 @@ impl YieldProviderClient for YoYie } fn yields(&self, asset_id: &AssetId) -> Vec { - self.vaults - .iter() - .filter_map(|vault| { - let vault_asset = vault.asset_id(); - if &vault_asset == asset_id { - Some(Yield::new(vault.name, vault_asset, self.provider(), None)) - } else { - None - } - }) + self.vaults_for_asset(asset_id) + .map(|vault| Yield::new(vault.name, vault.asset_id(), self.provider(), None)) .collect() } async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut results = Vec::new(); - for vault in self.vaults.iter().copied().filter(|vault: &YoVault| vault.asset_id() == *asset_id) { + for vault in self.vaults_for_asset(asset_id) { let gateway = self.gateway_for_chain(vault.chain)?; let lookback_blocks = lookback_blocks_for_chain(vault.chain); let data = gateway.fetch_position_data(vault, Address::ZERO, lookback_blocks).await?; @@ -88,10 +89,8 @@ impl YieldProviderClient for YoYie } async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let vault = self.find_vault(asset_id)?; - let gateway = self.gateway_for_chain(vault.chain)?; - let wallet = parse_address(wallet_address)?; - let amount = parse_value(value)?; + let (vault, gateway) = self.vault_and_gateway(asset_id)?; + let (wallet, amount) = parse_wallet_and_value(wallet_address, value)?; let approval = gateway.check_token_allowance(vault.asset_token, wallet, amount).await?; let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, U256::ZERO, wallet, YO_PARTNER_ID_GEM); @@ -99,10 +98,8 @@ impl YieldProviderClient for YoYie } async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let vault = self.find_vault(asset_id)?; - let gateway = self.gateway_for_chain(vault.chain)?; - let wallet = parse_address(wallet_address)?; - let assets = parse_value(value)?; + let (vault, gateway) = self.vault_and_gateway(asset_id)?; + let (wallet, assets) = parse_wallet_and_value(wallet_address, value)?; let shares = gateway.convert_to_shares(vault.yo_token, assets).await?; let approval = gateway.check_token_allowance(vault.yo_token, wallet, shares).await?; @@ -111,8 +108,7 @@ impl YieldProviderClient for YoYie } async fn positions(&self, request: &YieldDetailsRequest) -> Result { - let vault = self.find_vault(&request.asset_id)?; - let gateway = self.gateway_for_chain(vault.chain)?; + let (vault, gateway) = self.vault_and_gateway(&request.asset_id)?; let owner = parse_address(&request.wallet_address)?; let data = gateway.fetch_position_data(vault, owner, lookback_blocks_for_chain(vault.chain)).await?; @@ -141,6 +137,12 @@ fn parse_value(value: &str) -> Result { U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid value {value}: {err}"))) } +fn parse_wallet_and_value(wallet_address: &str, value: &str) -> Result<(Address, U256), YieldError> { + let wallet = parse_address(wallet_address)?; + let amount = parse_value(value)?; + Ok((wallet, amount)) +} + fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option) -> YieldTransaction { YieldTransaction { chain: vault.chain, diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index 6a48f06f5..d57e516c3 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -2,118 +2,89 @@ use std::{collections::HashMap, sync::Arc}; -use async_trait::async_trait; -use gem_client::ReqwestClient; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; +use gem_jsonrpc::{NativeProvider, RpcProvider}; use primitives::{Chain, EVMChain}; -use yielder::{ - YO_GATEWAY, YO_USD, YieldDetailsRequest, YieldError, YieldProvider, YieldProviderClient, Yielder, YoApiProvider, YoGatewayClient, YoPerformanceData, - YoProvider, YoYieldProvider, build_performance_url, parse_performance_response, -}; +use yielder::{YO_GATEWAY, YO_USD, YieldDetailsRequest, YieldProviderClient, Yielder, YoApiClient, YoGatewayClient, YoProvider, YoYieldProvider}; -fn base_rpc_url() -> String { - std::env::var("BASE_RPC_URL").unwrap_or_else(|_| "https://gemnodes.com/base".to_string()) +fn get_endpoint(provider: &NativeProvider, chain: Chain) -> String { + provider.get_endpoint(chain).unwrap_or_else(|err| panic!("missing RPC endpoint for chain {chain:?}: {err}")) +} + +fn build_gateways(provider: &NativeProvider) -> HashMap> { + let base_client = EthereumClient::new(JsonRpcClient::new_reqwest(get_endpoint(provider, Chain::Base)), EVMChain::Base); + let ethereum_client = EthereumClient::new(JsonRpcClient::new_reqwest(get_endpoint(provider, Chain::Ethereum)), EVMChain::Ethereum); + + println!("yielder: using gateway endpoints for Base and Ethereum"); + HashMap::from([ + (Chain::Base, Arc::new(YoGatewayClient::new(base_client, YO_GATEWAY)) as Arc), + (Chain::Ethereum, Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY)) as Arc), + ]) +} + +fn build_rpc_provider() -> Arc { + Arc::new(NativeProvider::new().set_debug(false)) } #[tokio::test] async fn test_yields_for_asset_with_apy() -> Result<(), Box> { - let jsonrpc_client = JsonRpcClient::new_reqwest(base_rpc_url()); - let ethereum_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); - let gateway_client: Arc = Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY)); - let mut gateways = HashMap::new(); - gateways.insert(Chain::Base, gateway_client); - let provider: Arc = Arc::new(YoYieldProvider::new(gateways)); + let rpc_provider = build_rpc_provider(); + let gateways = build_gateways(&rpc_provider); + let provider: Arc = Arc::new(YoYieldProvider::new(gateways, rpc_provider)); let yielder = Yielder::with_providers(vec![provider]); let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; + println!("yielder: yields_for_asset_with_apy count={}", apy_yields.len()); assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); let apy = apy_yields[0].apy.expect("apy should be computed"); + println!("yielder: first Yo APY={}", apy); assert!(apy.is_finite(), "apy should be finite"); assert!(apy > -1.0, "apy should be > -100%"); - let details = yielder - .positions( - YieldProvider::Yo, - &YieldDetailsRequest { - asset_id: YO_USD.asset_id(), - wallet_address: "0x0000000000000000000000000000000000000000".to_string(), - }, - ) - .await?; - - assert!(details.apy.is_some(), "apy should be present in details"); - Ok(()) } #[tokio::test] -async fn test_yo_api_performance() { - let url = build_performance_url( - Chain::Base, - "0x0000000f2eB9f69274678c76222B35eEc7588a65", - "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - ) - .expect("should build URL"); - - let client = reqwest::Client::new(); - let response = client.get(&url).send().await.expect("should fetch API"); - let data = response.bytes().await.expect("should get bytes"); - - let performance = parse_performance_response(&data).expect("should parse response"); - - println!("Yo API Performance:"); - println!(" Realized: {} (raw: {})", performance.realized.formatted, performance.realized.raw); - println!(" Unrealized: {} (raw: {})", performance.unrealized.formatted, performance.unrealized.raw); - println!(" Total rewards: {}", performance.total_rewards_raw()); - - assert!(performance.total_rewards_raw() > 0, "should have some rewards"); -} +async fn test_yo_api_performance() -> Result<(), Box> { + let rpc_provider = build_rpc_provider(); + let api_client = YoApiClient::new(rpc_provider); + + let vault_address = YO_USD.yo_token.to_string(); + let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; -struct ReqwestYoApiClient; - -#[async_trait] -impl YoApiProvider for ReqwestYoApiClient { - async fn get_user_performance(&self, chain: Chain, vault_address: &str, user_address: &str) -> Result { - let url = build_performance_url(chain, vault_address, user_address)?; - let client = reqwest::Client::new(); - let response = client.get(&url).send().await.map_err(|e| YieldError::new(e.to_string()))?; - let data = response.bytes().await.map_err(|e| YieldError::new(e.to_string()))?; - parse_performance_response(&data) - } + println!("yielder: fetch_rewards chain=Base vault={vault_address} wallet={wallet_address}"); + + let performance = api_client.fetch_rewards(Chain::Base, &vault_address, wallet_address).await?; + + println!("yielder: rewards total_raw={}", performance.total_rewards_raw(),); + assert!(performance.total_rewards_raw() > 0, "expected rewards for test address"); + + Ok(()) } #[tokio::test] -async fn test_yo_positions_with_rewards() { - let http_client = ReqwestClient::new_test_client(base_rpc_url()); - let jsonrpc_client = JsonRpcClient::new(http_client); - let eth_client = EthereumClient::new(jsonrpc_client, EVMChain::Base); - let gateway: Arc = Arc::new(YoGatewayClient::new(eth_client, YO_GATEWAY)); - let mut gateways = HashMap::new(); - gateways.insert(Chain::Base, gateway); - - let api_client: Arc = Arc::new(ReqwestYoApiClient); - let provider = YoYieldProvider::new(gateways).with_api_client(api_client); +async fn test_yo_positions_with_rewards() -> Result<(), Box> { + let rpc_provider = build_rpc_provider(); + let gateways = build_gateways(&rpc_provider); + let provider = YoYieldProvider::new(gateways, rpc_provider); let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; - let asset_id = YO_USD.asset_id(); - let request = YieldDetailsRequest { - asset_id: asset_id.clone(), + asset_id: YO_USD.asset_id(), wallet_address: wallet_address.to_string(), }; - let position = provider.positions(&request).await.expect("should fetch positions"); - - println!("Position for {wallet_address}:"); - println!(" Asset ID: {}", position.asset_id); - println!(" Provider: {:?}", position.provider); - println!(" Vault Token: {}", position.vault_token_address); - println!(" Asset Token: {}", position.asset_token_address); - println!(" Vault Balance (yoUSD shares): {:?}", position.vault_balance_value); - println!(" Asset Balance (USDC): {:?}", position.asset_balance_value); - println!(" APY: {:?}", position.apy); - println!(" Rewards: {:?}", position.rewards); + let position = provider.positions(&request).await?; + println!( + "yielder: position vault_balance={:?} asset_balance={:?} apy={:?} rewards={:?}", + position.vault_balance_value, position.asset_balance_value, position.apy, position.rewards + ); + assert!(position.vault_balance_value.is_some(), "vault balance should be present"); + assert!(position.asset_balance_value.is_some(), "asset balance should be present"); + assert!(position.apy.is_some(), "apy should be present"); assert!(position.rewards.is_some(), "rewards should be present"); + + Ok(()) } diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index f2b481224..6c504c8f8 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -14,7 +14,7 @@ name = "gemstone" [features] default = [] -reqwest_provider = ["dep:reqwest", "swapper/reqwest_provider"] +reqwest_provider = ["dep:reqwest", "gem_jsonrpc/reqwest"] swap_integration_tests = ["reqwest_provider"] [dependencies] diff --git a/gemstone/src/alien/error.rs b/gemstone/src/alien/error.rs index 10b168bb8..bd09dd1f8 100644 --- a/gemstone/src/alien/error.rs +++ b/gemstone/src/alien/error.rs @@ -2,7 +2,8 @@ pub type AlienError = swapper::AlienError; #[uniffi::remote(Enum)] pub enum AlienError { - RequestError { msg: String }, - ResponseError { msg: String }, - Http { status: u16, len: u32 }, + Network(String), + Timeout, + Http { status: u16, body: Vec }, + Serialization(String), } diff --git a/gemstone/src/alien/reqwest_provider.rs b/gemstone/src/alien/reqwest_provider.rs index 6ef081eba..7a67608b6 100644 --- a/gemstone/src/alien/reqwest_provider.rs +++ b/gemstone/src/alien/reqwest_provider.rs @@ -1,10 +1,8 @@ use super::{AlienError, AlienProvider, AlienTarget}; use async_trait::async_trait; -use gem_jsonrpc::{RpcProvider as GenericRpcProvider, RpcResponse}; +use gem_jsonrpc::{NativeProvider, RpcProvider as GenericRpcProvider, RpcResponse}; use primitives::Chain; -pub use swapper::NativeProvider; - #[async_trait] impl AlienProvider for NativeProvider { async fn request(&self, target: AlienTarget) -> Result { diff --git a/gemstone/src/gateway/error.rs b/gemstone/src/gateway/error.rs index 32725ed76..651b3ded7 100644 --- a/gemstone/src/gateway/error.rs +++ b/gemstone/src/gateway/error.rs @@ -24,9 +24,7 @@ impl Display for GatewayError { impl Error for GatewayError {} pub(crate) fn map_network_error(error: Box) -> GatewayError { - if let Some(jsonrpc_error) = error.downcast_ref::() - && jsonrpc_error.code == ERROR_CLIENT_ERROR - { + if let Some(jsonrpc_error) = error.downcast_ref::().filter(|candidate| candidate.code == ERROR_CLIENT_ERROR) { return GatewayError::NetworkError { msg: jsonrpc_error.message.clone(), }; @@ -50,15 +48,11 @@ fn http_status_from_error(error: &(dyn Error + 'static)) -> Option { let mut current_error: Option<&(dyn Error + 'static)> = Some(error); while let Some(err) = current_error { - if let Some(alien_error) = err.downcast_ref::() - && let AlienError::Http { status, .. } = alien_error - { + if let Some(AlienError::Http { status, .. }) = err.downcast_ref::() { return Some(*status); } - if let Some(client_error) = err.downcast_ref::() - && let gem_client::ClientError::Http { status, .. } = client_error - { + if let Some(gem_client::ClientError::Http { status, .. }) = err.downcast_ref::() { return Some(*status); } @@ -74,7 +68,7 @@ mod tests { #[test] fn test_map_network_error_with_status_code() { - let error = AlienError::Http { status: 404, len: 0 }; + let error = AlienError::Http { status: 404, body: Vec::new() }; let mapped = map_network_error(Box::new(error)); match mapped { diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index bb37aae91..217496f96 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -320,7 +320,6 @@ impl GemGateway { } pub async fn get_transaction_load(&self, chain: Chain, input: GemTransactionLoadInput, provider: Arc) -> Result { - // Prepare yield input (builds contract_address and call_data if needed) let input = if let Some(yielder) = &self.yielder { prepare_yield_input(yielder, input).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() })? } else { @@ -419,7 +418,7 @@ impl GemGateway { #[cfg(all(test, feature = "reqwest_provider"))] mod tests { use super::*; - use crate::alien::reqwest_provider::NativeProvider; + use gem_jsonrpc::native_provider::NativeProvider; #[tokio::test] async fn test_get_node_status_http_404_error() { diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 291dcf1b2..a7e48e0e7 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -12,7 +12,7 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, YieldTransaction, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -53,10 +53,7 @@ impl GemYielder { pub async fn positions(&self, provider: String, asset: AssetId, wallet_address: String) -> Result { let provider = provider.parse::()?; - let request = YieldDetailsRequest { - asset_id: asset, - wallet_address, - }; + let request = YieldDetailsRequest { asset_id: asset, wallet_address }; self.yielder.positions(provider, &request).await.map_err(Into::into) } @@ -71,15 +68,7 @@ impl GemYielder { chain_id: u64, ) -> Result { let provider = provider.parse::()?; - - let transaction = match action { - GemYieldAction::Deposit => { - self.yielder.deposit(provider, &asset, &wallet_address, &value).await? - } - GemYieldAction::Withdraw => { - self.yielder.withdraw(provider, &asset, &wallet_address, &value).await? - } - }; + let transaction = build_yield_transaction(&self.yielder, &action, provider, &asset, &wallet_address, &value).await?; Ok(GemYieldTransactionData { transaction, @@ -88,7 +77,6 @@ impl GemYielder { gas_limit: "300000".to_string(), }) } - } pub(crate) fn build_yielder(rpc_provider: Arc) -> Result { @@ -112,21 +100,11 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result Result { +pub(crate) async fn prepare_yield_input(yielder: &Yielder, input: GemTransactionLoadInput) -> Result { match &input.input_type { GemTransactionInputType::Yield { asset, action, data } => { if data.contract_address.is_empty() || data.call_data.is_empty() { - let transaction = match action { - GemYieldAction::Deposit => { - yielder.deposit(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await? - } - GemYieldAction::Withdraw => { - yielder.withdraw(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await? - } - }; + let transaction = build_yield_transaction(yielder, action, YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?; Ok(GemTransactionLoadInput { input_type: GemTransactionInputType::Yield { @@ -155,3 +133,17 @@ pub(crate) async fn prepare_yield_input( _ => Ok(input), } } + +async fn build_yield_transaction( + yielder: &Yielder, + action: &GemYieldAction, + provider: YieldProvider, + asset: &AssetId, + wallet_address: &str, + value: &str, +) -> Result { + match action { + GemYieldAction::Deposit => Ok(yielder.deposit(provider, asset, wallet_address, value).await?), + GemYieldAction::Withdraw => Ok(yielder.withdraw(provider, asset, wallet_address, value).await?), + } +} diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 47fa0b062..7d4c2419b 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -134,16 +134,6 @@ pub struct GemStakeData { pub to: Option, } -pub type GemEvmYieldData = primitives::EvmYieldData; - -#[uniffi::remote(Record)] -pub struct GemEvmYieldData { - pub contract_address: String, - pub call_data: String, - pub approval: Option, - pub gas_limit: Option, -} - #[uniffi::remote(Record)] pub struct GemHyperliquidOrder { pub approve_agent_required: bool, @@ -416,7 +406,7 @@ pub enum GemTransactionLoadMetadata { nonce: u64, chain_id: u64, stake_data: Option, - yield_data: Option, + yield_data: Option, }, Near { sequence: u64, @@ -501,7 +491,17 @@ impl From for GemTransactionLoadMetadata { TransactionLoadMetadata::Bitcoin { utxos } => GemTransactionLoadMetadata::Bitcoin { utxos }, TransactionLoadMetadata::Zcash { utxos, branch_id } => GemTransactionLoadMetadata::Zcash { utxos, branch_id }, TransactionLoadMetadata::Cardano { utxos } => GemTransactionLoadMetadata::Cardano { utxos }, - TransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data } => GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data }, + TransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data, + yield_data, + } => GemTransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data, + yield_data: yield_data.map(Into::into), + }, TransactionLoadMetadata::Near { sequence, block_hash } => GemTransactionLoadMetadata::Near { sequence, block_hash }, TransactionLoadMetadata::Stellar { sequence, @@ -589,7 +589,17 @@ impl From for TransactionLoadMetadata { GemTransactionLoadMetadata::Bitcoin { utxos } => TransactionLoadMetadata::Bitcoin { utxos }, GemTransactionLoadMetadata::Zcash { utxos, branch_id } => TransactionLoadMetadata::Zcash { utxos, branch_id }, GemTransactionLoadMetadata::Cardano { utxos } => TransactionLoadMetadata::Cardano { utxos }, - GemTransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data } => TransactionLoadMetadata::Evm { nonce, chain_id, stake_data, yield_data }, + GemTransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data, + yield_data, + } => TransactionLoadMetadata::Evm { + nonce, + chain_id, + stake_data, + yield_data: yield_data.map(Into::into), + }, GemTransactionLoadMetadata::Near { sequence, block_hash } => TransactionLoadMetadata::Near { sequence, block_hash }, GemTransactionLoadMetadata::Stellar { sequence, diff --git a/gemstone/tests/android/GemTest/app/src/main/java/com/example/gemtest/NativeProvider.kt b/gemstone/tests/android/GemTest/app/src/main/java/com/example/gemtest/NativeProvider.kt index 368183580..26f92bdc9 100644 --- a/gemstone/tests/android/GemTest/app/src/main/java/com/example/gemtest/NativeProvider.kt +++ b/gemstone/tests/android/GemTest/app/src/main/java/com/example/gemtest/NativeProvider.kt @@ -24,7 +24,7 @@ class NativeProvider: AlienProvider { val parsedUrl = try { Url(target.url) } catch (e: Throwable) { - throw AlienError.RequestError("invalid url: ${target.url}") + throw AlienError.Network("invalid url: ${target.url}") } val response = client.request { diff --git a/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift b/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift index 2706f0e02..2bc508895 100644 --- a/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift +++ b/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift @@ -7,7 +7,7 @@ public typealias SwapProvider = SwapperProvider extension AlienTarget: URLRequestConvertible { func asRequest() throws -> URLRequest { guard let url = URL(string: self.url) else { - let error = AlienError.RequestError(msg: "invalid url: \(self.url)") + let error = AlienError.Network("invalid url: \(self.url)") throw error } var request = URLRequest(url: url) diff --git a/gemstone/tests/ios/GemTest/GemTest/Networking/Provider.swift b/gemstone/tests/ios/GemTest/GemTest/Networking/Provider.swift index 258fa9aa1..fd7ab36dc 100644 --- a/gemstone/tests/ios/GemTest/GemTest/Networking/Provider.swift +++ b/gemstone/tests/ios/GemTest/GemTest/Networking/Provider.swift @@ -31,7 +31,7 @@ public actor NativeProvider { extension NativeProvider: AlienProvider { public nonisolated func getEndpoint(chain: String) throws -> String { guard let url = nodeConfig[chain] else { - throw AlienError.RequestError(msg: "\(chain) is not supported.") + throw AlienError.Network("\(chain) is not supported.") } return url.absoluteString } diff --git a/rustfmt.toml b/rustfmt.toml index 003b4b3f5..6c32d3491 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,3 @@ +style_edition = "2024" max_width = 180 reorder_imports = true From 9ea0029ba9490037199167f54a8ea7595d2a14de Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:20:06 +0900 Subject: [PATCH 23/33] Update GemstoneTest.kt --- .../src/androidTest/java/com/gemwallet/gemstone/GemstoneTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemstone/android/gemstone/src/androidTest/java/com/gemwallet/gemstone/GemstoneTest.kt b/gemstone/android/gemstone/src/androidTest/java/com/gemwallet/gemstone/GemstoneTest.kt index b5b461350..e4bad31c9 100644 --- a/gemstone/android/gemstone/src/androidTest/java/com/gemwallet/gemstone/GemstoneTest.kt +++ b/gemstone/android/gemstone/src/androidTest/java/com/gemwallet/gemstone/GemstoneTest.kt @@ -65,7 +65,7 @@ class GemstoneTest { @Test fun testProviderThrowsAlienException() = runBlocking { val errorMessage = "Request failed" - val provider = MockProvider { throw AlienException.RequestException(errorMessage) } + val provider = MockProvider { throw AlienException.Network(errorMessage) } val gateway = createGateway(provider) try { From 2330868f00901f96348bf0e5490360912ff5243c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:35:56 +0900 Subject: [PATCH 24/33] code cleanup --- Cargo.lock | 3 +- crates/gem_evm/src/multicall3.rs | 5 -- crates/gem_evm/src/provider/preload_mapper.rs | 13 +--- crates/primitives/src/stake_type.rs | 1 - .../primitives/src/transaction_input_type.rs | 4 +- crates/yielder/Cargo.toml | 13 ++-- crates/yielder/src/lib.rs | 4 +- crates/yielder/src/models.rs | 33 +-------- crates/yielder/src/provider.rs | 2 +- crates/yielder/src/yo/api/client.rs | 6 +- crates/yielder/src/yo/client.rs | 9 +-- crates/yielder/src/yo/error.rs | 8 +++ crates/yielder/src/yo/mod.rs | 4 +- crates/yielder/src/yo/provider.rs | 8 +-- crates/yielder/src/yo/vault.rs | 6 +- crates/yielder/tests/integration_test.rs | 8 +-- gemstone/Cargo.toml | 1 + gemstone/src/gem_yielder/mod.rs | 6 -- gemstone/src/lib.rs | 6 ++ gemstone/src/models/transaction.rs | 67 ++++--------------- 20 files changed, 68 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d780015d..9a79db931 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3624,6 +3624,7 @@ dependencies = [ "serde", "serde_json", "signer", + "strum", "sui-sdk-types", "swapper", "tokio", @@ -9214,12 +9215,12 @@ dependencies = [ "gem_client", "gem_evm", "gem_jsonrpc", - "num-traits", "primitives", "reqwest 0.13.1", "serde", "serde_json", "serde_serializers", + "strum", "tokio", ] diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 92480ba0a..5e74befdd 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -27,13 +27,11 @@ sol! { } } -/// Handle returned when adding a call to the batch. Used to decode the result. pub struct CallHandle { index: usize, _marker: PhantomData, } -/// Results from executing a multicall batch pub struct Multicall3Results { results: Vec, } @@ -51,7 +49,6 @@ impl Multicall3Results { } } -/// Builder for constructing multicall3 batches pub struct Multicall3Builder<'a, C: Client + Clone> { client: &'a EthereumClient, calls: Vec, @@ -67,7 +64,6 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { } } - /// Add a contract call to the batch pub fn add(&mut self, target: Address, call: T) -> CallHandle { let index = self.calls.len(); self.calls.push(IMulticall3::Call3 { @@ -78,7 +74,6 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { CallHandle { index, _marker: PhantomData } } - /// Set the block number to execute at (default: latest) pub fn at_block(mut self, block: u64) -> Self { self.block = Some(block); self diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index 8537034f1..4b724cb2d 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -60,9 +60,7 @@ pub fn map_transaction_fee_rates(chain: EVMChain, fee_history: &EthereumFeeHisto .into_iter() .map(|x| { let priority_fee = BigInt::max(min_priority_fee.clone(), x.value.clone()); - // maxFeePerGas must be >= maxPriorityFeePerGas, so use base_fee + priority_fee - let max_fee_per_gas = base_fee.clone() + &priority_fee; - FeeRate::new(x.priority, GasPriceType::eip1559(max_fee_per_gas, priority_fee)) + FeeRate::new(x.priority, GasPriceType::eip1559(base_fee.clone(), priority_fee)) }) .collect()) } @@ -384,8 +382,6 @@ mod tests { GasPriceType::Eip1559 { gas_price, priority_fee } => { assert!(*gas_price >= min_priority_fee); assert!(*priority_fee >= min_priority_fee); - // EIP-1559: maxFeePerGas must be >= maxPriorityFeePerGas - assert!(*gas_price >= *priority_fee, "maxFeePerGas must be >= maxPriorityFeePerGas"); } _ => panic!("Expected EIP-1559 gas price type"), } @@ -406,11 +402,8 @@ mod tests { let result = map_transaction_fee_rates(EVMChain::SmartChain, &fee_history)?; assert_eq!(result.len(), 3); - - // When base_fee is 0, max_fee_per_gas equals priority_fee (0x5f5e100 = 100000000) - let expected_priority_fee = BigInt::from(100000000u64); - assert_eq!(result[0].gas_price_type.gas_price(), expected_priority_fee.clone()); - assert_eq!(result[0].gas_price_type.priority_fee(), expected_priority_fee); + assert_eq!(result[0].gas_price_type.gas_price(), BigInt::ZERO); + assert!(result[0].gas_price_type.priority_fee() != BigInt::ZERO); Ok(()) } diff --git a/crates/primitives/src/stake_type.rs b/crates/primitives/src/stake_type.rs index 1bcf1168a..643217fae 100644 --- a/crates/primitives/src/stake_type.rs +++ b/crates/primitives/src/stake_type.rs @@ -13,7 +13,6 @@ pub struct RedelegateData { #[derive(Debug, Clone, Serialize, Deserialize)] #[typeshare(swift = "Equatable, Sendable, Hashable")] -#[serde(rename_all = "camelCase")] pub struct StakeData { pub data: Option, pub to: Option, diff --git a/crates/primitives/src/transaction_input_type.rs b/crates/primitives/src/transaction_input_type.rs index ec67fc655..8f8ccc419 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -78,8 +78,8 @@ impl TransactionInputType { PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, TransactionInputType::Yield(_, action, _) => match action { - YieldAction::Deposit => TransactionType::StakeDelegate, - YieldAction::Withdraw => TransactionType::StakeUndelegate, + YieldAction::Deposit => TransactionType::YieldDeposit, + YieldAction::Withdraw => TransactionType::YieldWithdraw, }, } } diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index 6573cffa9..0b14442c1 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -3,14 +3,14 @@ name = "yielder" version.workspace = true edition.workspace = true license.workspace = true -homepage.workspace = true -description.workspace = true -repository.workspace = true -documentation.workspace = true [features] default = [] -yield_integration_tests = ["gem_jsonrpc/reqwest", "gem_client/reqwest", "tokio/rt-multi-thread"] +yield_integration_tests = [ + "gem_jsonrpc/reqwest", + "gem_client/reqwest", + "tokio/rt-multi-thread", +] [dependencies] alloy-primitives = { workspace = true } @@ -21,10 +21,9 @@ gem_jsonrpc = { path = "../gem_jsonrpc" } primitives = { path = "../primitives" } serde_serializers = { path = "../serde_serializers" } async-trait = { workspace = true } -num-traits = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = ["macros"] } +strum = { workspace = true } [dev-dependencies] gem_client = { path = "../gem_client", features = ["reqwest"] } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 8aed2ef74..c0d9b24fb 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,6 +5,6 @@ pub mod yo; pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ - IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USD, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, YoVault, YoYieldProvider, - vaults, + BoxError, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, YoVault, + YoYieldProvider, vaults, }; diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index 9be7552b1..81a93fe7d 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -1,40 +1,13 @@ -use std::{fmt, str::FromStr}; - use alloy_primitives::Address; use primitives::{AssetId, Chain, swap::ApprovalData}; +use strum::{AsRefStr, Display, EnumString}; -use crate::yo::YieldError; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr)] +#[strum(serialize_all = "lowercase")] pub enum YieldProvider { Yo, } -impl YieldProvider { - pub fn name(&self) -> &'static str { - match self { - YieldProvider::Yo => "yo", - } - } -} - -impl fmt::Display for YieldProvider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.name()) - } -} - -impl FromStr for YieldProvider { - type Err = YieldError; - - fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - "yo" => Ok(YieldProvider::Yo), - other => Err(YieldError::new(format!("unknown yield provider {other}"))), - } - } -} - #[derive(Debug, Clone)] pub struct Yield { pub name: String, diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 6f60a13bc..f94e14b60 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -80,6 +80,6 @@ impl Yielder { .iter() .find(|candidate| candidate.provider() == provider) .cloned() - .ok_or_else(|| YieldError::new(format!("provider {provider} not found"))) + .ok_or_else(|| format!("provider {provider} not found").into()) } } diff --git a/crates/yielder/src/yo/api/client.rs b/crates/yielder/src/yo/api/client.rs index 0148b8f75..73f794fb2 100644 --- a/crates/yielder/src/yo/api/client.rs +++ b/crates/yielder/src/yo/api/client.rs @@ -21,7 +21,7 @@ impl YoApiClient { let network = match chain { Chain::Base => "base", Chain::Ethereum => "ethereum", - _ => return Err(YieldError::new(format!("unsupported chain for Yo API: {:?}", chain))), + _ => return Err(format!("unsupported chain for Yo API: {:?}", chain).into()), }; let url = format!("{}/api/v1/performance/user/{}/{}/{}", YO_API_BASE_URL, network, vault_address, user_address); let target = Target::get(&url); @@ -30,10 +30,10 @@ impl YoApiClient { .rpc_provider .request(target) .await - .map_err(|e| YieldError::new(format!("fetch performance error: request failed: {e}")))?; + .map_err(|e| format!("fetch performance error: request failed: {e}"))?; let parsed: YoApiResponse = - serde_json::from_slice(&response.data).map_err(|e| YieldError::new(format!("fetch performance error: failed to parse response: {e}")))?; + serde_json::from_slice(&response.data).map_err(|e| format!("fetch performance error: parse failed: {e}"))?; if parsed.status_code != 200 { return Ok(YoPerformanceData::default()); diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 3ed7e2d1c..df18bfed2 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -96,7 +96,7 @@ where .ethereum_client .get_latest_block() .await - .map_err(|err| YieldError::new(format!("failed to fetch latest block: {err}")))?; + .map_err(|e| format!("failed to fetch latest block: {e}"))?; let lookback_block = latest_block.saturating_sub(lookback_blocks); let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); @@ -156,9 +156,10 @@ where .ethereum_client .eth_call(&self.contract_address.to_string(), &call_data) .await - .map_err(|e| YieldError::new(format!("convert_to_shares eth_call failed: {e}")))?; - let bytes = hex::decode(&result).map_err(|e| YieldError::new(format!("convert_to_shares hex decode failed: {e}")))?; - let shares = IYoGateway::quoteConvertToSharesCall::abi_decode_returns(&bytes).map_err(|e| YieldError::new(format!("convert_to_shares abi decode failed: {e}")))?; + .map_err(|e| format!("convert_to_shares eth_call failed: {e}"))?; + let bytes = hex::decode(&result).map_err(|e| format!("convert_to_shares hex decode failed: {e}"))?; + let shares = + IYoGateway::quoteConvertToSharesCall::abi_decode_returns(&bytes).map_err(|e| format!("convert_to_shares abi decode failed: {e}"))?; Ok(shares) } } diff --git a/crates/yielder/src/yo/error.rs b/crates/yielder/src/yo/error.rs index 5ce0e008d..94e3529c2 100644 --- a/crates/yielder/src/yo/error.rs +++ b/crates/yielder/src/yo/error.rs @@ -2,6 +2,8 @@ use std::{error::Error, fmt}; use gem_evm::multicall3::Multicall3Error; +pub type BoxError = Box; + #[derive(Debug, Clone)] pub struct YieldError(String); @@ -40,3 +42,9 @@ impl From for YieldError { YieldError::new(e.to_string()) } } + +impl From for YieldError { + fn from(e: BoxError) -> Self { + YieldError::new(e.to_string()) + } +} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index d879652da..05e710323 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -9,10 +9,10 @@ mod vault; pub use api::{YoApiClient, YoPerformanceData}; pub use client::{YoGatewayClient, YoProvider}; pub use contract::{IYoGateway, IYoVaultToken}; -pub use error::YieldError; +pub use error::{BoxError, YieldError}; pub use model::PositionData; pub use provider::YoYieldProvider; -pub use vault::{YO_USD, YO_USDT, YoVault, vaults}; +pub use vault::{YO_USDC, YO_USDT, YoVault, vaults}; use alloy_primitives::{Address, address}; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 3f083c452..5ab727291 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -40,7 +40,7 @@ impl YoYieldProvider { fn find_vault(&self, asset_id: &AssetId) -> Result { self.vaults_for_asset(asset_id) .next() - .ok_or_else(|| YieldError::new(format!("unsupported asset {}", asset_id))) + .ok_or_else(|| format!("unsupported asset {}", asset_id).into()) } fn vaults_for_asset(&self, asset_id: &AssetId) -> impl Iterator + '_ { @@ -51,7 +51,7 @@ impl YoYieldProvider { fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YieldError> { self.gateways .get(&chain) - .ok_or_else(|| YieldError::new(format!("no gateway configured for chain {:?}", chain))) + .ok_or_else(|| format!("no gateway configured for chain {:?}", chain).into()) } fn vault_and_gateway(&self, asset_id: &AssetId) -> Result<(YoVault, &Arc), YieldError> { @@ -130,11 +130,11 @@ impl YieldProviderClient for YoYie } fn parse_address(value: &str) -> Result { - Address::from_str(value).map_err(|err| YieldError::new(format!("invalid address {value}: {err}"))) + Address::from_str(value).map_err(|e| format!("invalid address {value}: {e}").into()) } fn parse_value(value: &str) -> Result { - U256::from_str_radix(value, 10).map_err(|err| YieldError::new(format!("invalid value {value}: {err}"))) + U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}").into()) } fn parse_wallet_and_value(wallet_address: &str, value: &str) -> Result<(Address, U256), YieldError> { diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index ed123b30f..57f790692 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -26,8 +26,8 @@ impl YoVault { } } -pub const YO_USD: YoVault = YoVault::new( - "yoUSD", +pub const YO_USDC: YoVault = YoVault::new( + "yoUSDC", Chain::Base, address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), @@ -43,5 +43,5 @@ pub const YO_USDT: YoVault = YoVault::new( ); pub fn vaults() -> &'static [YoVault] { - &[YO_USD, YO_USDT] + &[YO_USDC, YO_USDT] } diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index d57e516c3..4bda71c7d 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -6,7 +6,7 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::{NativeProvider, RpcProvider}; use primitives::{Chain, EVMChain}; -use yielder::{YO_GATEWAY, YO_USD, YieldDetailsRequest, YieldProviderClient, Yielder, YoApiClient, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{YO_GATEWAY, YO_USDC, YieldDetailsRequest, YieldProviderClient, Yielder, YoApiClient, YoGatewayClient, YoProvider, YoYieldProvider}; fn get_endpoint(provider: &NativeProvider, chain: Chain) -> String { provider.get_endpoint(chain).unwrap_or_else(|err| panic!("missing RPC endpoint for chain {chain:?}: {err}")) @@ -34,7 +34,7 @@ async fn test_yields_for_asset_with_apy() -> Result<(), Box = Arc::new(YoYieldProvider::new(gateways, rpc_provider)); let yielder = Yielder::with_providers(vec![provider]); - let apy_yields = yielder.yields_for_asset_with_apy(&YO_USD.asset_id()).await?; + let apy_yields = yielder.yields_for_asset_with_apy(&YO_USDC.asset_id()).await?; println!("yielder: yields_for_asset_with_apy count={}", apy_yields.len()); assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); let apy = apy_yields[0].apy.expect("apy should be computed"); @@ -50,7 +50,7 @@ async fn test_yo_api_performance() -> Result<(), Box Result<(), Box) -> std::fmt::Result { - f.debug_struct("GemYielder").finish() - } -} - #[uniffi::export] impl GemYielder { #[uniffi::constructor] diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index 153cb3ee4..18fd2e5c6 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -114,6 +114,12 @@ impl From for GemstoneError { } } +impl From for GemstoneError { + fn from(error: strum::ParseError) -> Self { + Self::AnyError { msg: error.to_string() } + } +} + impl From for GemstoneError { fn from(error: gateway::GatewayError) -> Self { Self::AnyError { msg: error.to_string() } diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 7d4c2419b..a65f7aeff 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -5,6 +5,7 @@ use primitives::{ AccountDataType, Asset, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, StakeType, TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, UInt64, WalletConnectionSessionAppMetadata, + YieldAction, YieldData, perpetual::{CancelOrderData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, TPSLOrderData}, }; use std::collections::HashMap; @@ -243,14 +244,18 @@ pub enum PerpetualType { Reduce(PerpetualReduceData), } -#[derive(Debug, Clone, uniffi::Enum)] -pub enum GemYieldAction { +pub type GemYieldAction = YieldAction; + +#[uniffi::remote(Enum)] +pub enum YieldAction { Deposit, Withdraw, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct GemYieldData { +pub type GemYieldData = YieldData; + +#[uniffi::remote(Record)] +pub struct YieldData { pub provider_name: String, pub contract_address: String, pub call_data: String, @@ -500,7 +505,7 @@ impl From for GemTransactionLoadMetadata { nonce, chain_id, stake_data, - yield_data: yield_data.map(Into::into), + yield_data, }, TransactionLoadMetadata::Near { sequence, block_hash } => GemTransactionLoadMetadata::Near { sequence, block_hash }, TransactionLoadMetadata::Stellar { @@ -598,7 +603,7 @@ impl From for TransactionLoadMetadata { nonce, chain_id, stake_data, - yield_data: yield_data.map(Into::into), + yield_data, }, GemTransactionLoadMetadata::Near { sequence, block_hash } => TransactionLoadMetadata::Near { sequence, block_hash }, GemTransactionLoadMetadata::Stellar { @@ -702,11 +707,7 @@ impl From for GemTransactionInputType { TransactionInputType::TransferNft(asset, nft_asset) => GemTransactionInputType::TransferNft { asset, nft_asset }, TransactionInputType::Account(asset, account_type) => GemTransactionInputType::Account { asset, account_type }, TransactionInputType::Perpetual(asset, perpetual_type) => GemTransactionInputType::Perpetual { asset, perpetual_type }, - TransactionInputType::Yield(asset, action, data) => GemTransactionInputType::Yield { - asset, - action: action.into(), - data: data.into(), - }, + TransactionInputType::Yield(asset, action, data) => GemTransactionInputType::Yield { asset, action, data }, } } } @@ -858,7 +859,7 @@ impl From for TransactionInputType { GemTransactionInputType::TransferNft { asset, nft_asset } => TransactionInputType::TransferNft(asset, nft_asset), GemTransactionInputType::Account { asset, account_type } => TransactionInputType::Account(asset, account_type), GemTransactionInputType::Perpetual { asset, perpetual_type } => TransactionInputType::Perpetual(asset, perpetual_type), - GemTransactionInputType::Yield { asset, action, data } => TransactionInputType::Yield(asset, action.into(), data.into()), + GemTransactionInputType::Yield { asset, action, data } => TransactionInputType::Yield(asset, action, data), } } } @@ -880,45 +881,3 @@ impl From for GemFreezeData { } } } - -impl From for primitives::YieldAction { - fn from(value: GemYieldAction) -> Self { - match value { - GemYieldAction::Deposit => primitives::YieldAction::Deposit, - GemYieldAction::Withdraw => primitives::YieldAction::Withdraw, - } - } -} - -impl From for GemYieldAction { - fn from(value: primitives::YieldAction) -> Self { - match value { - primitives::YieldAction::Deposit => GemYieldAction::Deposit, - primitives::YieldAction::Withdraw => GemYieldAction::Withdraw, - } - } -} - -impl From for primitives::YieldData { - fn from(value: GemYieldData) -> Self { - primitives::YieldData { - provider_name: value.provider_name, - contract_address: value.contract_address, - call_data: value.call_data, - approval: value.approval, - gas_limit: value.gas_limit, - } - } -} - -impl From for GemYieldData { - fn from(value: primitives::YieldData) -> Self { - GemYieldData { - provider_name: value.provider_name, - contract_address: value.contract_address, - call_data: value.call_data, - approval: value.approval, - gas_limit: value.gas_limit, - } - } -} From 494b19800d760dd4a01400c33edcc24c014e6b09 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:14:42 +0900 Subject: [PATCH 25/33] renaming variables and cleanup --- apps/daemon/src/pusher/pusher.rs | 2 +- crates/gem_evm/src/multicall3.rs | 3 - crates/primitives/src/asset_metadata.rs | 4 +- crates/primitives/src/hex.rs | 10 +++ crates/primitives/src/transaction.rs | 8 +- .../primitives/src/transaction_input_type.rs | 4 +- crates/primitives/src/transaction_type.rs | 4 +- crates/yielder/src/lib.rs | 4 +- crates/yielder/src/provider.rs | 33 ++------- crates/yielder/src/yo/client.rs | 4 +- crates/yielder/src/yo/mod.rs | 2 +- crates/yielder/src/yo/provider.rs | 74 +++++++++---------- gemstone/src/gem_yielder/mod.rs | 14 +--- gemstone/src/models/transaction.rs | 4 +- 14 files changed, 73 insertions(+), 97 deletions(-) diff --git a/apps/daemon/src/pusher/pusher.rs b/apps/daemon/src/pusher/pusher.rs index 4318bea1c..1cceb968f 100644 --- a/apps/daemon/src/pusher/pusher.rs +++ b/apps/daemon/src/pusher/pusher.rs @@ -122,7 +122,7 @@ impl Pusher { title: localizer.notification_unfreeze_title(self.get_value(amount, asset.symbol.clone()).as_str()), message: None, }), - TransactionType::YieldDeposit | TransactionType::YieldWithdraw => Err("Yield transactions not implemented".into()), + TransactionType::EarnDeposit | TransactionType::EarnWithdraw => Err("Earn transactions not implemented".into()), } } diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 5e74befdd..6f5fc9c87 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -37,7 +37,6 @@ pub struct Multicall3Results { } impl Multicall3Results { - /// Decode the result for a specific call handle pub fn decode(&self, handle: &CallHandle) -> Result { let result = self.results.get(handle.index).ok_or_else(|| Multicall3Error(format!("invalid index: {}", handle.index)))?; @@ -79,7 +78,6 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { self } - /// Execute all calls in a single RPC request pub async fn execute(self) -> Result { if self.calls.is_empty() { return Ok(Multicall3Results { results: vec![] }); @@ -129,7 +127,6 @@ pub fn deployment_by_chain_stack(stack: ChainStack) -> &'static str { } } -// Helpers for direct Call3 creation (used by swapper crate) pub fn create_call3(target: &str, call: impl SolCall) -> IMulticall3::Call3 { IMulticall3::Call3 { target: target.parse().unwrap(), diff --git a/crates/primitives/src/asset_metadata.rs b/crates/primitives/src/asset_metadata.rs index 1c5ac0300..62695f140 100644 --- a/crates/primitives/src/asset_metadata.rs +++ b/crates/primitives/src/asset_metadata.rs @@ -10,9 +10,9 @@ pub struct AssetMetaData { pub is_buy_enabled: bool, pub is_sell_enabled: bool, pub is_swap_enabled: bool, - pub is_stake_enabled: bool, + pub is_earn_enabled: bool, pub is_pinned: bool, pub is_active: bool, - pub staking_apr: Option, + pub earn_apr: Option, pub rank_score: i32, } diff --git a/crates/primitives/src/hex.rs b/crates/primitives/src/hex.rs index 46e69a0b4..9442cc46a 100644 --- a/crates/primitives/src/hex.rs +++ b/crates/primitives/src/hex.rs @@ -31,6 +31,10 @@ pub fn decode_hex(value: &str) -> Result, HexError> { Ok(hex::decode(&*normalized)?) } +pub fn encode_with_0x(data: &[u8]) -> String { + format!("0x{}", hex::encode(data)) +} + #[cfg(test)] mod tests { use super::*; @@ -46,4 +50,10 @@ mod tests { let bytes = decode_hex("0xa").expect("decode"); assert_eq!(bytes, vec![0x0a]); } + + #[test] + fn encode_with_0x_adds_prefix() { + assert_eq!(encode_with_0x(&[0x0a, 0x0b]), "0x0a0b"); + assert_eq!(encode_with_0x(&[]), "0x"); + } } diff --git a/crates/primitives/src/transaction.rs b/crates/primitives/src/transaction.rs index 285e1794b..e7d9cacc2 100644 --- a/crates/primitives/src/transaction.rs +++ b/crates/primitives/src/transaction.rs @@ -261,8 +261,8 @@ impl Transaction { | TransactionType::PerpetualOpenPosition | TransactionType::PerpetualClosePosition | TransactionType::PerpetualModifyPosition - | TransactionType::YieldDeposit - | TransactionType::YieldWithdraw => vec![self.asset_id.clone(), self.fee_asset_id.clone()], + | TransactionType::EarnDeposit + | TransactionType::EarnWithdraw => vec![self.asset_id.clone(), self.fee_asset_id.clone()], TransactionType::Swap => self .metadata .clone() @@ -298,8 +298,8 @@ impl Transaction { | TransactionType::PerpetualOpenPosition | TransactionType::PerpetualClosePosition | TransactionType::PerpetualModifyPosition - | TransactionType::YieldDeposit - | TransactionType::YieldWithdraw => vec![AssetAddress::new(self.asset_id.clone(), self.to.clone(), None)], + | TransactionType::EarnDeposit + | TransactionType::EarnWithdraw => vec![AssetAddress::new(self.asset_id.clone(), self.to.clone(), None)], TransactionType::Swap => self .metadata .clone() diff --git a/crates/primitives/src/transaction_input_type.rs b/crates/primitives/src/transaction_input_type.rs index 8f8ccc419..74c28d9aa 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -78,8 +78,8 @@ impl TransactionInputType { PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, TransactionInputType::Yield(_, action, _) => match action { - YieldAction::Deposit => TransactionType::YieldDeposit, - YieldAction::Withdraw => TransactionType::YieldWithdraw, + YieldAction::Deposit => TransactionType::EarnDeposit, + YieldAction::Withdraw => TransactionType::EarnWithdraw, }, } } diff --git a/crates/primitives/src/transaction_type.rs b/crates/primitives/src/transaction_type.rs index c4910eb3b..0eefd012f 100644 --- a/crates/primitives/src/transaction_type.rs +++ b/crates/primitives/src/transaction_type.rs @@ -27,8 +27,8 @@ pub enum TransactionType { PerpetualOpenPosition, PerpetualClosePosition, PerpetualModifyPosition, - YieldDeposit, - YieldWithdraw, + EarnDeposit, + EarnWithdraw, } impl TransactionType { diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index c0d9b24fb..4925f668e 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,6 +5,6 @@ pub mod yo; pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ - BoxError, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, YoVault, - YoYieldProvider, vaults, + BoxError, GAS_LIMIT, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, + YoVault, YoYieldProvider, vaults, }; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index f94e14b60..2c730b2e7 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -18,64 +18,43 @@ pub trait YieldProviderClient: Send + Sync { } } -#[derive(Default)] pub struct Yielder { providers: Vec>, } impl Yielder { - pub fn new() -> Self { - Self { providers: Vec::new() } - } - - pub fn with_providers(providers: Vec>) -> Self { + pub fn new(providers: Vec>) -> Self { Self { providers } } - pub fn add_provider

(&mut self, provider: P) - where - P: YieldProviderClient + 'static, - { - self.providers.push(Arc::new(provider)); - } - - pub fn add_provider_arc(&mut self, provider: Arc) { - self.providers.push(provider); - } - pub fn yields_for_asset(&self, asset_id: &AssetId) -> Vec { self.providers.iter().flat_map(|provider| provider.yields(asset_id)).collect() } - pub fn is_yield_available(&self, asset_id: &AssetId) -> bool { - self.providers.iter().any(|provider| !provider.yields(asset_id).is_empty()) - } - pub async fn yields_for_asset_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut yields = Vec::new(); for provider in &self.providers { - let mut provider_yields = provider.yields_with_apy(asset_id).await?; - yields.append(&mut provider_yields); + yields.extend(provider.yields_with_apy(asset_id).await?); } Ok(yields) } pub async fn deposit(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let provider = self.provider(provider)?; + let provider = self.get_provider(provider)?; provider.deposit(asset_id, wallet_address, value).await } pub async fn withdraw(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let provider = self.provider(provider)?; + let provider = self.get_provider(provider)?; provider.withdraw(asset_id, wallet_address, value).await } pub async fn positions(&self, provider: YieldProvider, request: &YieldDetailsRequest) -> Result { - let provider = self.provider(provider)?; + let provider = self.get_provider(provider)?; provider.positions(request).await } - fn provider(&self, provider: YieldProvider) -> Result, YieldError> { + fn get_provider(&self, provider: YieldProvider) -> Result, YieldError> { self.providers .iter() .find(|candidate| candidate.provider() == provider) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index df18bfed2..a49c5ce24 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -18,7 +18,7 @@ pub trait YoProvider: Send + Sync { fn contract_address(&self) -> Address; fn build_deposit_transaction(&self, from: Address, yo_vault: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; fn build_redeem_transaction(&self, from: Address, yo_vault: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; - async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; + async fn get_position(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result; async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YieldError>; async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result; } @@ -91,7 +91,7 @@ where TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn fetch_position_data(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { + async fn get_position(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { let latest_block = self .ethereum_client .get_latest_block() diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index 05e710323..d2b43a635 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -11,7 +11,7 @@ pub use client::{YoGatewayClient, YoProvider}; pub use contract::{IYoGateway, IYoVaultToken}; pub use error::{BoxError, YieldError}; pub use model::PositionData; -pub use provider::YoYieldProvider; +pub use provider::{GAS_LIMIT, YoYieldProvider}; pub use vault::{YO_USDC, YO_USDT, YoVault, vaults}; use alloy_primitives::{Address, address}; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 5ab727291..4a78003b8 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -12,6 +12,8 @@ use crate::provider::YieldProviderClient; use super::api::YoApiClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; +pub const GAS_LIMIT: &str = "300000"; + const SECONDS_PER_YEAR: f64 = 31_536_000.0; fn lookback_blocks_for_chain(chain: Chain) -> u64 { @@ -53,12 +55,6 @@ impl YoYieldProvider { .get(&chain) .ok_or_else(|| format!("no gateway configured for chain {:?}", chain).into()) } - - fn vault_and_gateway(&self, asset_id: &AssetId) -> Result<(YoVault, &Arc), YieldError> { - let vault = self.find_vault(asset_id)?; - let gateway = self.gateway_for_chain(vault.chain)?; - Ok((vault, gateway)) - } } #[async_trait] @@ -79,7 +75,7 @@ impl YieldProviderClient for YoYie for vault in self.vaults_for_asset(asset_id) { let gateway = self.gateway_for_chain(vault.chain)?; let lookback_blocks = lookback_blocks_for_chain(vault.chain); - let data = gateway.fetch_position_data(vault, Address::ZERO, lookback_blocks).await?; + let data = gateway.get_position(vault, Address::ZERO, lookback_blocks).await?; let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); @@ -89,8 +85,10 @@ impl YieldProviderClient for YoYie } async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let (vault, gateway) = self.vault_and_gateway(asset_id)?; - let (wallet, amount) = parse_wallet_and_value(wallet_address, value)?; + let vault = self.find_vault(asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; + let wallet = Address::from_str(wallet_address).map_err(|e| format!("invalid address {wallet_address}: {e}"))?; + let amount = U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}"))?; let approval = gateway.check_token_allowance(vault.asset_token, wallet, amount).await?; let tx = gateway.build_deposit_transaction(wallet, vault.yo_token, amount, U256::ZERO, wallet, YO_PARTNER_ID_GEM); @@ -98,8 +96,10 @@ impl YieldProviderClient for YoYie } async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let (vault, gateway) = self.vault_and_gateway(asset_id)?; - let (wallet, assets) = parse_wallet_and_value(wallet_address, value)?; + let vault = self.find_vault(asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; + let wallet = Address::from_str(wallet_address).map_err(|e| format!("invalid address {wallet_address}: {e}"))?; + let assets = U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}"))?; let shares = gateway.convert_to_shares(vault.yo_token, assets).await?; let approval = gateway.check_token_allowance(vault.yo_token, wallet, shares).await?; @@ -108,41 +108,37 @@ impl YieldProviderClient for YoYie } async fn positions(&self, request: &YieldDetailsRequest) -> Result { - let (vault, gateway) = self.vault_and_gateway(&request.asset_id)?; - let owner = parse_address(&request.wallet_address)?; - let data = gateway.fetch_position_data(vault, owner, lookback_blocks_for_chain(vault.chain)).await?; + let vault = self.find_vault(&request.asset_id)?; + let gateway = self.gateway_for_chain(vault.chain)?; + let owner = Address::from_str(&request.wallet_address).map_err(|e| format!("invalid address {}: {e}", request.wallet_address))?; + let data = gateway.get_position(vault, owner, lookback_blocks_for_chain(vault.chain)).await?; let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); let asset_value = data.share_balance.saturating_mul(data.latest_price) / one_share; let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); - - let mut position = YieldPosition::new(vault.name, request.asset_id.clone(), self.provider(), vault.yo_token, vault.asset_token); - position.vault_balance_value = Some(data.share_balance.to_string()); - position.asset_balance_value = Some(asset_value.to_string()); - position.apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); - - if let Ok(performance) = self.api_client.fetch_rewards(vault.chain, &vault.yo_token.to_string(), &request.wallet_address).await { - position.rewards = Some(performance.total_rewards_raw().to_string()); - } - - Ok(position) + let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); + + let rewards = self + .api_client + .fetch_rewards(vault.chain, &vault.yo_token.to_string(), &request.wallet_address) + .await + .ok() + .map(|p| p.total_rewards_raw().to_string()); + + Ok(YieldPosition { + name: vault.name.to_string(), + asset_id: request.asset_id.clone(), + provider: self.provider(), + vault_token_address: vault.yo_token.to_string(), + asset_token_address: vault.asset_token.to_string(), + vault_balance_value: Some(data.share_balance.to_string()), + asset_balance_value: Some(asset_value.to_string()), + apy, + rewards, + }) } } -fn parse_address(value: &str) -> Result { - Address::from_str(value).map_err(|e| format!("invalid address {value}: {e}").into()) -} - -fn parse_value(value: &str) -> Result { - U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}").into()) -} - -fn parse_wallet_and_value(wallet_address: &str, value: &str) -> Result<(Address, U256), YieldError> { - let wallet = parse_address(wallet_address)?; - let amount = parse_value(value)?; - Ok((wallet, amount)) -} - fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option) -> YieldTransaction { YieldTransaction { chain: vault.chain, diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 5f4402fc2..a62850a80 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -12,7 +12,7 @@ use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, YieldTransaction, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, YieldTransaction, Yielder, YoGatewayClient, YoProvider, YoYieldProvider, GAS_LIMIT}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -31,10 +31,6 @@ impl GemYielder { self.yielder.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) } - pub fn is_yield_available(&self, asset_id: &AssetId) -> bool { - self.yielder.is_yield_available(asset_id) - } - pub async fn deposit(&self, provider: String, asset: AssetId, wallet_address: String, value: String) -> Result { let provider = provider.parse::()?; self.yielder.deposit(provider, &asset, &wallet_address, &value).await.map_err(Into::into) @@ -68,7 +64,7 @@ impl GemYielder { transaction, nonce, chain_id, - gas_limit: "300000".to_string(), + gas_limit: GAS_LIMIT.to_string(), }) } } @@ -89,9 +85,7 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result = Arc::new(YoYieldProvider::new(gateways, wrapper)); - let mut yielder = Yielder::new(); - yielder.add_provider_arc(yo_provider); - Ok(yielder) + Ok(Yielder::new(vec![yo_provider])) } pub(crate) async fn prepare_yield_input(yielder: &Yielder, input: GemTransactionLoadInput) -> Result { @@ -109,7 +103,7 @@ pub(crate) async fn prepare_yield_input(yielder: &Yielder, input: GemTransaction contract_address: transaction.to, call_data: transaction.data, approval: transaction.approval, - gas_limit: Some("350000".to_string()), + gas_limit: Some(GAS_LIMIT.to_string()), }, }, sender_address: input.sender_address, diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index a65f7aeff..53f921e13 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -106,8 +106,8 @@ pub enum TransactionType { PerpetualOpenPosition, PerpetualClosePosition, PerpetualModifyPosition, - YieldDeposit, - YieldWithdraw, + EarnDeposit, + EarnWithdraw, } pub type GemAccountDataType = AccountDataType; From ce59fd6669b3ce429330d2b6e5b548d8f97ac29f Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:43:53 +0900 Subject: [PATCH 26/33] merge StakeData and YieldData and more cleanup --- .claude/commands/push_pr.md | 55 ------------------- AGENTS.md | 44 ++++++--------- crates/gem_evm/src/multicall3.rs | 11 ++-- crates/gem_evm/src/provider/preload.rs | 15 ++--- crates/gem_evm/src/provider/preload_mapper.rs | 29 ++++------ .../src/native_provider/reqwest.rs | 2 +- crates/primitives/src/lib.rs | 4 +- crates/primitives/src/stake_type.rs | 7 --- .../primitives/src/transaction_input_type.rs | 4 +- .../src/transaction_load_metadata.rs | 5 +- crates/primitives/src/yield_data.rs | 30 ++++++++-- crates/yielder/src/yo/provider.rs | 16 ++---- gemstone/src/gem_yielder/mod.rs | 12 ++-- gemstone/src/models/transaction.rs | 55 +++++-------------- 14 files changed, 96 insertions(+), 193 deletions(-) delete mode 100644 .claude/commands/push_pr.md diff --git a/.claude/commands/push_pr.md b/.claude/commands/push_pr.md deleted file mode 100644 index e929b5b5a..000000000 --- a/.claude/commands/push_pr.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -description: "Automatically create branch, commit changes, and create a pull request" -usage: "/push_pr [issue-number]" -examples: - - "/push_pr" - - "/push_pr 123" ---- - -# Push PR Workflow (Automatic) - -Automatically create a new branch, commit all changes, and create a pull request with smart defaults. - -## Arguments -- `[issue-number]`: Optional issue number to reference in the PR (e.g., `/push_pr 123`) - -## Automatic Behavior - -The command will automatically: - -1. **Generate branch name** based on changed files and content -2. **Create commit message** by analyzing the changes -3. **Add all changes** to staging area -4. **Create new branch** with generated name -5. **Commit changes** with generated message -6. **Push branch** to remote repository -7. **Create pull request** with descriptive title and body - -## Smart Defaults - -- **Branch naming:** `feat/auto-TIMESTAMP` or `fix/auto-TIMESTAMP` based on changes -- **Commit messages:** Generated from file changes and content analysis -- **PR titles:** Descriptive titles based on the changes made -- **PR descriptions:** Includes summary of changes and test information - -## Usage Examples - -```bash -# Simple usage - everything automated -/push_pr - -# With issue reference -/push_pr 456 -``` - -## Implementation - -I'll analyze the current git changes, generate appropriate branch names and commit messages, then execute the full workflow automatically. - -The commit message will automatically include the Claude Code attribution as per the repository's commit conventions. - ---- - -**Arguments received:** `$ARGUMENTS` - -Let me execute the automated push PR workflow, analyzing the current changes to generate appropriate branch name and commit message. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 867dec1ff..bb1617e82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,14 +84,11 @@ Individual `gem_*` crates for each blockchain with unified RPC client patterns: ## Technology Stack -- Framework: Rust workspace with Rocket web framework -- Database: PostgreSQL (primary), Redis (caching) -- Message Queue: RabbitMQ with Lapin -- RPC: Custom `gem_jsonrpc` client library for blockchain interactions -- Mobile: UniFFI for iOS/Android bindings -- Serialization: Serde with custom serializers -- Async: Tokio runtime -- Testing: Built-in Rust testing with integration tests +- **Framework**: Rust workspace with Rocket, Tokio async runtime +- **Database**: PostgreSQL with Diesel ORM, Redis caching +- **Message Queue**: RabbitMQ with Lapin +- **Mobile**: UniFFI for iOS/Android bindings +- **Serialization**: Serde with custom serializers ## Development Workflow @@ -164,6 +161,11 @@ Follow the existing code style patterns unless explicitly asked to change ### Commit Messages - Write descriptive messages following conventional commit format +### Code Style +- **Prefer immutability**: Avoid `mut` when possible. Use functional patterns like `map()`, `filter()`, `fold()`, and method chaining instead of mutable accumulators +- **Minimal comments**: Do not add comments unless absolutely necessary. Code should be self-documenting through clear naming and structure. Comments are acceptable only for non-obvious business logic or external API quirks +- **No dead code**: Remove unused functions, variables, and imports immediately. Don't comment out code "for later" + ### Naming and Conventions - Files/modules: `snake_case` (e.g., `asset_id.rs`, `chain_address.rs`) - Crates: Prefixed naming (`gem_*` for blockchains, `security_*` for security) @@ -172,7 +174,7 @@ Follow the existing code style patterns unless explicitly asked to change - Constants: `SCREAMING_SNAKE_CASE` - Helper names: inside a module stick to concise names that rely on scope rather than repeating crate/module prefixes (e.g., prefer `is_spot_swap` over `is_hypercore_spot_swap` in `core_signer.rs`). - Don't use `util`, `utils`, `normalize`, or any other similar names for modules or functions. -- Avoid using `matches!` for pattern matching as much as possible, it's easy to missing a case later. +- Avoid using `matches!` for pattern matching as much as possible, it's easy to miss a case later. ### Imports 1. Standard library imports first @@ -192,24 +194,18 @@ IMPORTANT: Always import models and types at the top of the file. Never use inli ### Database Patterns - Separate database models from domain primitives - Use `as_primitive()` methods for conversion -- Diesel ORM with PostgreSQL backend - Support transactions and upserts ### Async Patterns -- Tokio runtime throughout - Async client structs returning `Result` - Use `Arc>` for shared async state ## Architecture & Patterns ### Key Development Patterns -- One crate per blockchain using unified RPC client patterns -- UniFFI bindings require careful Rust API design for mobile compatibility - Use `BigDecimal` for financial precision -- Use async/await with Tokio across services -- Database models use Diesel ORM with automatic migrations -- Consider cross-platform performance constraints for mobile -- Shared U256 conversions: prefer `u256_to_biguint` and `biguint_to_u256` from `crates/gem_evm/src/u256.rs` for Alloy `U256` <-> `BigUint` conversions. +- Consider cross-platform performance constraints for mobile (UniFFI bindings require careful Rust API design) +- Shared U256 conversions: prefer `u256_to_biguint` and `biguint_to_u256` from `crates/gem_evm/src/u256.rs` for Alloy `U256` <-> `BigUint` conversions ### Repository Pattern @@ -253,13 +249,11 @@ Direct repository access methods available on `DatabaseClient` include: - And more... ### RPC Client Patterns -- Use `gem_jsonrpc::JsonRpcClient` for blockchain RPC interactions - Prefer `alloy_primitives::hex::encode_prefixed()` for hex encoding with `0x` prefix - **Always use `alloy_primitives::hex::decode()` for hex decoding** - it handles `0x` prefix automatically - Use `alloy_primitives::Address::to_string()` instead of manual formatting - RPC calls expect hex strings directly; avoid double encoding - Use `JsonRpcClient::batch_call()` for batch operations -- Propagate errors via `JsonRpcError` ### Blockchain Provider Patterns - Each blockchain crate has a `provider/` directory with trait implementations @@ -270,19 +264,13 @@ Direct repository access methods available on `DatabaseClient` include: ## Testing -### Conventions -- Place integration tests in `tests/` directories +- Place integration tests in `tests/` directories with layout: `src/`, `tests/`, `testdata/` - Use `#[tokio::test]` for async tests - Prefix test names descriptively with `test_` - Use `Result<(), Box>` for test error handling - Configure integration tests with `test = false` and appropriate `required-features` for manual execution -- Prefer real networks for RPC client tests (e.g., Ethereum mainnet) -- Test data management: For long JSON test data (>20 lines), store in `testdata/` and load with `include_str!()`; per-crate layout is typically `src/`, `tests/`, `testdata/` - -### Integration Testing -- Add integration tests for RPC functionality to verify real network compatibility -- Prefer recent blocks for batch operations (more reliable than historical blocks) -- Verify both successful calls and proper error propagation +- Prefer real networks and recent blocks for RPC client tests +- Test data: store long JSON (>20 lines) in `testdata/` and load with `include_str!()` - Use realistic contract addresses (e.g., USDC) for `eth_call` testing ## Task Completion diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 6f5fc9c87..9e2b6c398 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -1,9 +1,10 @@ use std::{fmt, marker::PhantomData}; -use alloy_primitives::{Address, hex}; +use alloy_primitives::Address; use alloy_sol_types::{SolCall, sol}; use gem_client::Client; use primitives::chain_config::ChainStack; +use primitives::hex; use serde_json::json; use crate::rpc::EthereumClient; @@ -83,7 +84,7 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { return Ok(Multicall3Results { results: vec![] }); } - let multicall_address = deployment_by_chain_stack(self.client.chain.chain_stack()); + let address = deployment_by_chain_stack(self.client.chain.chain_stack()); let multicall_data = IMulticall3::aggregate3Call { calls: self.calls }.abi_encode(); let block_param = self.block.map(|n| serde_json::Value::String(format!("0x{n:x}"))).unwrap_or_else(|| json!("latest")); @@ -94,14 +95,14 @@ impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { .call( "eth_call", json!([{ - "to": multicall_address, - "data": hex::encode_prefixed(&multicall_data) + "to": address, + "data": hex::encode_with_0x(&multicall_data) }, block_param]), ) .await .map_err(|e| Multicall3Error(e.to_string()))?; - let result_data = hex::decode(&result).map_err(|e| Multicall3Error(e.to_string()))?; + let result_data = hex::decode_hex(&result).map_err(|e| Multicall3Error(e.to_string()))?; let results = IMulticall3::aggregate3Call::abi_decode_returns(&result_data).map_err(|e| Multicall3Error(e.to_string()))?; diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index 0ef4ea220..5189fd34f 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -15,9 +15,7 @@ use gem_client::Client; use num_bigint::BigInt; use primitives::GasPriceType; #[cfg(feature = "rpc")] -use primitives::stake_type::StakeData; -#[cfg(feature = "rpc")] -use primitives::{FeeRate, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput}; +use primitives::{EarnData, FeeRate, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput}; #[cfg(feature = "rpc")] use serde_serializers::bigint::bigint_from_hex_str; use std::collections::HashMap; @@ -68,20 +66,15 @@ impl EthereumClient { TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { nonce, chain_id, - stake_data: Some(StakeData { - data: if params.data.is_empty() { None } else { Some(hex::encode(¶ms.data)) }, - to: Some(params.to), - }), - yield_data: None, + earn_data: Some(EarnData::stake(params.to, ¶ms.data)), }, _ => input.metadata, }, - TransactionInputType::Yield(_, _, yield_input) => match input.metadata { + TransactionInputType::Yield(_, _, earn_input) => match input.metadata { TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { nonce, chain_id, - stake_data: None, - yield_data: Some(yield_input.clone()), + earn_data: Some(earn_input.clone()), }, _ => input.metadata, }, diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index 4b724cb2d..d7de95c36 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -46,8 +46,7 @@ pub fn map_transaction_preload(nonce_hex: String, chain_id: String) -> Result()?, - stake_data: None, - yield_data: None, + earn_data: None, }) } @@ -143,16 +142,18 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> } _ => Err("Unsupported chain for staking".into()), }, - TransactionInputType::Yield(_, action, yield_data) => { - if let Some(approval) = &yield_data.approval { + TransactionInputType::Yield(_, action, earn_data) => { + if let Some(approval) = &earn_data.approval { Ok(TransactionParams::new(approval.token.clone(), encode_erc20_approve(&approval.spender)?, BigInt::from(0))) } else { - let call_data = hex::decode(&yield_data.call_data)?; + let call_data = earn_data.call_data.as_ref().ok_or("Missing call_data")?; + let contract_address = earn_data.contract_address.as_ref().ok_or("Missing contract_address")?; + let decoded_data = hex::decode(call_data)?; let tx_value = match action { YieldAction::Deposit => BigInt::from(0), YieldAction::Withdraw => BigInt::from(0), }; - Ok(TransactionParams::new(yield_data.contract_address.clone(), call_data, tx_value)) + Ok(TransactionParams::new(contract_address.clone(), decoded_data, tx_value)) } } _ => Err("Unsupported transfer type".into()), @@ -193,9 +194,9 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { - if yield_data.approval.is_some() && yield_data.gas_limit.is_some() { - Ok(BigInt::from_str_radix(yield_data.gas_limit.as_ref().unwrap(), 10)?) + TransactionInputType::Yield(_, _, earn_data) => { + if earn_data.approval.is_some() && earn_data.gas_limit.is_some() { + Ok(BigInt::from_str_radix(earn_data.gas_limit.as_ref().unwrap(), 10)?) } else { Ok(BigInt::from(0)) } @@ -322,16 +323,10 @@ mod tests { let result = map_transaction_preload(nonce_hex, chain_id)?; match result { - TransactionLoadMetadata::Evm { - nonce, - chain_id, - stake_data, - yield_data, - } => { + TransactionLoadMetadata::Evm { nonce, chain_id, earn_data } => { assert_eq!(nonce, 10); assert_eq!(chain_id, 1); - assert!(stake_data.is_none()); - assert!(yield_data.is_none()); + assert!(earn_data.is_none()); } _ => panic!("Expected Evm variant"), } diff --git a/crates/gem_jsonrpc/src/native_provider/reqwest.rs b/crates/gem_jsonrpc/src/native_provider/reqwest.rs index f2f16c949..de41aa5ff 100644 --- a/crates/gem_jsonrpc/src/native_provider/reqwest.rs +++ b/crates/gem_jsonrpc/src/native_provider/reqwest.rs @@ -54,7 +54,7 @@ impl RpcProvider for NativeProvider { HttpMethod::Delete => self.client.delete(target.url), HttpMethod::Head => self.client.head(target.url), HttpMethod::Patch => self.client.patch(target.url), - HttpMethod::Options => return Err(ClientError::Network("options method not supported".to_string())), + HttpMethod::Options => self.client.request(reqwest::Method::OPTIONS, target.url), }; if let Some(headers) = target.headers { diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 897a42ef3..7b2a48e02 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -220,7 +220,7 @@ pub use self::transaction_preload_input::TransactionPreloadInput; pub mod transaction_fee; pub use self::transaction_fee::{FeeOption, TransactionFee}; pub mod stake_type; -pub use self::stake_type::{RedelegateData, StakeData, StakeType}; +pub use self::stake_type::{RedelegateData, StakeType}; pub mod transaction_load_metadata; pub use self::transaction_load_metadata::{HyperliquidOrder, TransactionLoadMetadata}; pub mod transaction_input_type; @@ -228,7 +228,7 @@ pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData pub mod transfer_data_extra; pub use self::transfer_data_extra::TransferDataExtra; pub mod yield_data; -pub use self::yield_data::{YieldAction, YieldData}; +pub use self::yield_data::{EarnData, YieldAction}; pub mod transaction_data_output; pub use self::transaction_data_output::{TransferDataOutputAction, TransferDataOutputType}; pub mod broadcast_options; diff --git a/crates/primitives/src/stake_type.rs b/crates/primitives/src/stake_type.rs index 643217fae..065f1d967 100644 --- a/crates/primitives/src/stake_type.rs +++ b/crates/primitives/src/stake_type.rs @@ -11,13 +11,6 @@ pub struct RedelegateData { pub to_validator: DelegationValidator, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Sendable, Hashable")] -pub struct StakeData { - pub data: Option, - pub to: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, EnumString)] #[typeshare(swift = "Equatable, Sendable, Hashable")] #[serde(rename_all = "camelCase")] diff --git a/crates/primitives/src/transaction_input_type.rs b/crates/primitives/src/transaction_input_type.rs index 74c28d9aa..75f9606da 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -2,7 +2,7 @@ use crate::stake_type::StakeType; use crate::swap::{ApprovalData, SwapData}; use crate::transaction_fee::TransactionFee; use crate::transaction_load_metadata::TransactionLoadMetadata; -use crate::yield_data::{YieldAction, YieldData}; +use crate::yield_data::{EarnData, YieldAction}; use crate::{ Asset, GasPriceType, PerpetualType, TransactionPreloadInput, TransactionType, TransferDataExtra, WalletConnectionSessionAppMetadata, nft::NFTAsset, perpetual::AccountDataType, }; @@ -22,7 +22,7 @@ pub enum TransactionInputType { TransferNft(Asset, NFTAsset), Account(Asset, AccountDataType), Perpetual(Asset, PerpetualType), - Yield(Asset, YieldAction, YieldData), + Yield(Asset, YieldAction, EarnData), } impl TransactionInputType { diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index 80298048b..17a250c42 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData, yield_data::YieldData}; +use crate::{UTXO, solana_token_program::SolanaTokenProgramId, yield_data::EarnData}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HyperliquidOrder { @@ -46,8 +46,7 @@ pub enum TransactionLoadMetadata { Evm { nonce: u64, chain_id: u64, - stake_data: Option, - yield_data: Option, + earn_data: Option, }, Near { sequence: u64, diff --git a/crates/primitives/src/yield_data.rs b/crates/primitives/src/yield_data.rs index 179240438..a334cb3e4 100644 --- a/crates/primitives/src/yield_data.rs +++ b/crates/primitives/src/yield_data.rs @@ -13,10 +13,32 @@ pub enum YieldAction { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[typeshare(swift = "Equatable, Hashable, Sendable")] #[serde(rename_all = "camelCase")] -pub struct YieldData { - pub provider_name: String, - pub contract_address: String, - pub call_data: String, +pub struct EarnData { + pub provider: Option, + pub contract_address: Option, + pub call_data: Option, pub approval: Option, pub gas_limit: Option, } + +impl EarnData { + pub fn stake(contract_address: String, call_data: &[u8]) -> Self { + Self { + provider: None, + contract_address: Some(contract_address), + call_data: if call_data.is_empty() { None } else { Some(hex::encode(call_data)) }, + approval: None, + gas_limit: None, + } + } + + pub fn yield_data(provider: String, contract_address: String, call_data: String, approval: Option, gas_limit: Option) -> Self { + Self { + provider: Some(provider), + contract_address: Some(contract_address), + call_data: Some(call_data), + approval, + gas_limit, + } + } +} diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 4a78003b8..78c4ee5a9 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -39,10 +39,8 @@ impl YoYieldProvider { } } - fn find_vault(&self, asset_id: &AssetId) -> Result { - self.vaults_for_asset(asset_id) - .next() - .ok_or_else(|| format!("unsupported asset {}", asset_id).into()) + fn get_vault(&self, asset_id: &AssetId) -> Result { + self.vaults_for_asset(asset_id).next().ok_or_else(|| format!("unsupported asset {}", asset_id).into()) } fn vaults_for_asset(&self, asset_id: &AssetId) -> impl Iterator + '_ { @@ -51,9 +49,7 @@ impl YoYieldProvider { } fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YieldError> { - self.gateways - .get(&chain) - .ok_or_else(|| format!("no gateway configured for chain {:?}", chain).into()) + self.gateways.get(&chain).ok_or_else(|| format!("no gateway configured for chain {:?}", chain).into()) } } @@ -85,7 +81,7 @@ impl YieldProviderClient for YoYie } async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let vault = self.find_vault(asset_id)?; + let vault = self.get_vault(asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; let wallet = Address::from_str(wallet_address).map_err(|e| format!("invalid address {wallet_address}: {e}"))?; let amount = U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}"))?; @@ -96,7 +92,7 @@ impl YieldProviderClient for YoYie } async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { - let vault = self.find_vault(asset_id)?; + let vault = self.get_vault(asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; let wallet = Address::from_str(wallet_address).map_err(|e| format!("invalid address {wallet_address}: {e}"))?; let assets = U256::from_str_radix(value, 10).map_err(|e| format!("invalid value {value}: {e}"))?; @@ -108,7 +104,7 @@ impl YieldProviderClient for YoYie } async fn positions(&self, request: &YieldDetailsRequest) -> Result { - let vault = self.find_vault(&request.asset_id)?; + let vault = self.get_vault(&request.asset_id)?; let gateway = self.gateway_for_chain(vault.chain)?; let owner = Address::from_str(&request.wallet_address).map_err(|e| format!("invalid address {}: {e}", request.wallet_address))?; let data = gateway.get_position(vault, owner, lookback_blocks_for_chain(vault.chain)).await?; diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index a62850a80..433efdb0e 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ GemstoneError, alien::{AlienProvider, AlienProviderWrapper}, - models::{GemTransactionInputType, GemTransactionLoadInput, GemYieldData}, + models::{GemEarnData, GemTransactionInputType, GemTransactionLoadInput}, }; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; @@ -91,17 +91,17 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result Result { match &input.input_type { GemTransactionInputType::Yield { asset, action, data } => { - if data.contract_address.is_empty() || data.call_data.is_empty() { + if data.contract_address.is_none() || data.call_data.is_none() { let transaction = build_yield_transaction(yielder, action, YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?; Ok(GemTransactionLoadInput { input_type: GemTransactionInputType::Yield { asset: asset.clone(), action: action.clone(), - data: GemYieldData { - provider_name: data.provider_name.clone(), - contract_address: transaction.to, - call_data: transaction.data, + data: GemEarnData { + provider: data.provider.clone(), + contract_address: Some(transaction.to), + call_data: Some(transaction.data), approval: transaction.approval, gas_limit: Some(GAS_LIMIT.to_string()), }, diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 53f921e13..55f93097e 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -1,11 +1,11 @@ use crate::models::*; use num_bigint::BigInt; -use primitives::stake_type::{FreezeData, StakeData}; +use primitives::stake_type::FreezeData; use primitives::{ - AccountDataType, Asset, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, StakeType, TransactionChange, - TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, + AccountDataType, Asset, EarnData, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, StakeType, + TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, UInt64, WalletConnectionSessionAppMetadata, - YieldAction, YieldData, + YieldAction, perpetual::{CancelOrderData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, TPSLOrderData}, }; use std::collections::HashMap; @@ -127,14 +127,6 @@ pub struct GemTransactionStateRequest { pub type GemHyperliquidOrder = HyperliquidOrder; -pub type GemStakeData = StakeData; - -#[uniffi::remote(Record)] -pub struct GemStakeData { - pub data: Option, - pub to: Option, -} - #[uniffi::remote(Record)] pub struct GemHyperliquidOrder { pub approve_agent_required: bool, @@ -252,13 +244,13 @@ pub enum YieldAction { Withdraw, } -pub type GemYieldData = YieldData; +pub type GemEarnData = EarnData; #[uniffi::remote(Record)] -pub struct YieldData { - pub provider_name: String, - pub contract_address: String, - pub call_data: String, +pub struct EarnData { + pub provider: Option, + pub contract_address: Option, + pub call_data: Option, pub approval: Option, pub gas_limit: Option, } @@ -305,7 +297,7 @@ pub enum GemTransactionInputType { Yield { asset: GemAsset, action: GemYieldAction, - data: GemYieldData, + data: GemEarnData, }, } @@ -410,8 +402,7 @@ pub enum GemTransactionLoadMetadata { Evm { nonce: u64, chain_id: u64, - stake_data: Option, - yield_data: Option, + earn_data: Option, }, Near { sequence: u64, @@ -496,17 +487,7 @@ impl From for GemTransactionLoadMetadata { TransactionLoadMetadata::Bitcoin { utxos } => GemTransactionLoadMetadata::Bitcoin { utxos }, TransactionLoadMetadata::Zcash { utxos, branch_id } => GemTransactionLoadMetadata::Zcash { utxos, branch_id }, TransactionLoadMetadata::Cardano { utxos } => GemTransactionLoadMetadata::Cardano { utxos }, - TransactionLoadMetadata::Evm { - nonce, - chain_id, - stake_data, - yield_data, - } => GemTransactionLoadMetadata::Evm { - nonce, - chain_id, - stake_data, - yield_data, - }, + TransactionLoadMetadata::Evm { nonce, chain_id, earn_data } => GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data }, TransactionLoadMetadata::Near { sequence, block_hash } => GemTransactionLoadMetadata::Near { sequence, block_hash }, TransactionLoadMetadata::Stellar { sequence, @@ -594,17 +575,7 @@ impl From for TransactionLoadMetadata { GemTransactionLoadMetadata::Bitcoin { utxos } => TransactionLoadMetadata::Bitcoin { utxos }, GemTransactionLoadMetadata::Zcash { utxos, branch_id } => TransactionLoadMetadata::Zcash { utxos, branch_id }, GemTransactionLoadMetadata::Cardano { utxos } => TransactionLoadMetadata::Cardano { utxos }, - GemTransactionLoadMetadata::Evm { - nonce, - chain_id, - stake_data, - yield_data, - } => TransactionLoadMetadata::Evm { - nonce, - chain_id, - stake_data, - yield_data, - }, + GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data } => TransactionLoadMetadata::Evm { nonce, chain_id, earn_data }, GemTransactionLoadMetadata::Near { sequence, block_hash } => TransactionLoadMetadata::Near { sequence, block_hash }, GemTransactionLoadMetadata::Stellar { sequence, From dfbe2738c2e97b9ba68a7ad5d641baf100221364 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:02:32 +0900 Subject: [PATCH 27/33] add risk level --- crates/yielder/src/lib.rs | 2 +- crates/yielder/src/models.rs | 12 +++++++++++- crates/yielder/src/provider.rs | 4 ++++ crates/yielder/src/yo/provider.rs | 4 ++-- crates/yielder/src/yo/vault.rs | 8 +++++++- gemstone/src/gem_yielder/remote_types.rs | 12 +++++++++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 4925f668e..ac17ea7f0 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -2,7 +2,7 @@ mod models; mod provider; pub mod yo; -pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +pub use models::{RiskLevel, Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ BoxError, GAS_LIMIT, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index 81a93fe7d..06baf12a0 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -8,21 +8,31 @@ pub enum YieldProvider { Yo, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Display, EnumString, AsRefStr)] +#[strum(serialize_all = "lowercase")] +pub enum RiskLevel { + Low, + Medium, + High, +} + #[derive(Debug, Clone)] pub struct Yield { pub name: String, pub asset_id: AssetId, pub provider: YieldProvider, pub apy: Option, + pub risk: RiskLevel, } impl Yield { - pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option) -> Self { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option, risk: RiskLevel) -> Self { Self { name: name.into(), asset_id, provider, apy, + risk, } } } diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 2c730b2e7..a4d6205bb 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -36,6 +36,10 @@ impl Yielder { for provider in &self.providers { yields.extend(provider.yields_with_apy(asset_id).await?); } + yields.sort_by(|a, b| { + let apy_cmp = b.apy.partial_cmp(&a.apy).unwrap_or(std::cmp::Ordering::Equal); + apy_cmp.then_with(|| a.risk.cmp(&b.risk)) + }); Ok(yields) } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 78c4ee5a9..1ec29d34b 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -61,7 +61,7 @@ impl YieldProviderClient for YoYie fn yields(&self, asset_id: &AssetId) -> Vec { self.vaults_for_asset(asset_id) - .map(|vault| Yield::new(vault.name, vault.asset_id(), self.provider(), None)) + .map(|vault| Yield::new(vault.name, vault.asset_id(), self.provider(), None, vault.risk)) .collect() } @@ -74,7 +74,7 @@ impl YieldProviderClient for YoYie let data = gateway.get_position(vault, Address::ZERO, lookback_blocks).await?; let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); - results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); + results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy, vault.risk)); } Ok(results) diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index 57f790692..58b9a0a61 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -1,6 +1,8 @@ use alloy_primitives::{Address, address}; use primitives::{AssetId, Chain}; +use crate::models::RiskLevel; + #[derive(Debug, Clone, Copy)] pub struct YoVault { pub name: &'static str, @@ -8,16 +10,18 @@ pub struct YoVault { pub yo_token: Address, pub asset_token: Address, pub asset_decimals: u8, + pub risk: RiskLevel, } impl YoVault { - pub const fn new(name: &'static str, chain: Chain, yo_token: Address, asset_token: Address, asset_decimals: u8) -> Self { + pub const fn new(name: &'static str, chain: Chain, yo_token: Address, asset_token: Address, asset_decimals: u8, risk: RiskLevel) -> Self { Self { name, chain, yo_token, asset_token, asset_decimals, + risk, } } @@ -32,6 +36,7 @@ pub const YO_USDC: YoVault = YoVault::new( address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), 6, + RiskLevel::Medium, ); pub const YO_USDT: YoVault = YoVault::new( @@ -40,6 +45,7 @@ pub const YO_USDT: YoVault = YoVault::new( address!("0xb9a7da9e90d3b428083bae04b860faa6325b721e"), address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), 6, + RiskLevel::Medium, ); pub fn vaults() -> &'static [YoVault] { diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index d75a4a375..dfc89b651 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,5 +1,5 @@ use primitives::AssetId; -use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; +use yielder::{RiskLevel, Yield, YieldPosition, YieldProvider, YieldTransaction}; use crate::models::swap::GemApprovalData; pub use crate::models::transaction::GemYieldAction; @@ -11,6 +11,15 @@ pub enum GemYieldProvider { Yo, } +pub type GemRiskLevel = RiskLevel; + +#[uniffi::remote(Enum)] +pub enum GemRiskLevel { + Low, + Medium, + High, +} + #[derive(Debug, Clone, uniffi::Record)] pub struct GemYieldTransactionData { pub transaction: GemYieldTransaction, @@ -27,6 +36,7 @@ pub struct GemYield { pub asset_id: AssetId, pub provider: GemYieldProvider, pub apy: Option, + pub risk: GemRiskLevel, } pub type GemYieldTransaction = YieldTransaction; From 7a08add1816343842aab5d14f8dc447094c8b4da Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:18:57 +0900 Subject: [PATCH 28/33] add back is_stake_enabled --- crates/primitives/src/asset_metadata.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/primitives/src/asset_metadata.rs b/crates/primitives/src/asset_metadata.rs index 62695f140..8e331f046 100644 --- a/crates/primitives/src/asset_metadata.rs +++ b/crates/primitives/src/asset_metadata.rs @@ -10,9 +10,11 @@ pub struct AssetMetaData { pub is_buy_enabled: bool, pub is_sell_enabled: bool, pub is_swap_enabled: bool, + pub is_stake_enabled: bool, + pub staking_apr: Option, pub is_earn_enabled: bool, + pub earn_apr: Option, pub is_pinned: bool, pub is_active: bool, - pub earn_apr: Option, pub rank_score: i32, } From 131dee26d05ed16cecd92fd9631f656ce36ae4a7 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:13:56 +0900 Subject: [PATCH 29/33] remove yo api client --- crates/gem_evm/src/u256.rs | 9 ++++ crates/yielder/src/lib.rs | 3 +- crates/yielder/src/yo/api/client.rs | 44 ---------------- crates/yielder/src/yo/api/mod.rs | 5 -- crates/yielder/src/yo/api/model.rs | 32 ----------- crates/yielder/src/yo/mod.rs | 2 - crates/yielder/src/yo/model.rs | 25 +++++++++ crates/yielder/src/yo/provider.rs | 64 +++++----------------- crates/yielder/tests/integration_test.rs | 67 ++++++++++-------------- gemstone/src/gem_yielder/mod.rs | 2 +- 10 files changed, 77 insertions(+), 176 deletions(-) delete mode 100644 crates/yielder/src/yo/api/client.rs delete mode 100644 crates/yielder/src/yo/api/mod.rs delete mode 100644 crates/yielder/src/yo/api/model.rs diff --git a/crates/gem_evm/src/u256.rs b/crates/gem_evm/src/u256.rs index 15eb1f2e8..7bf7e11cf 100644 --- a/crates/gem_evm/src/u256.rs +++ b/crates/gem_evm/src/u256.rs @@ -13,3 +13,12 @@ pub fn biguint_to_u256(value: &BigUint) -> Option { Some(U256::from_be_slice(&bytes)) } + +pub fn u256_to_f64(value: U256) -> f64 { + let limbs = value.as_limbs(); + let low = limbs[0] as f64; + let mid_low = limbs[1] as f64 * 2f64.powi(64); + let mid_high = limbs[2] as f64 * 2f64.powi(128); + let high = limbs[3] as f64 * 2f64.powi(192); + low + mid_low + mid_high + high +} diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index ac17ea7f0..7a996350d 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,6 +5,5 @@ pub mod yo; pub use models::{RiskLevel, Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ - BoxError, GAS_LIMIT, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoApiClient, YoGatewayClient, YoPerformanceData, YoProvider, - YoVault, YoYieldProvider, vaults, + BoxError, GAS_LIMIT, IYoGateway, IYoVaultToken, YO_GATEWAY, YO_PARTNER_ID_GEM, YO_USDC, YO_USDT, YieldError, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults, }; diff --git a/crates/yielder/src/yo/api/client.rs b/crates/yielder/src/yo/api/client.rs deleted file mode 100644 index 73f794fb2..000000000 --- a/crates/yielder/src/yo/api/client.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::sync::Arc; - -use gem_jsonrpc::{RpcProvider, Target}; -use primitives::Chain; - -use super::model::{YoApiResponse, YoPerformanceData}; -use crate::yo::YieldError; - -const YO_API_BASE_URL: &str = "https://api.yo.xyz"; - -pub struct YoApiClient { - rpc_provider: Arc>, -} - -impl YoApiClient { - pub fn new(rpc_provider: Arc>) -> Self { - Self { rpc_provider } - } - - pub async fn fetch_rewards(&self, chain: Chain, vault_address: &str, user_address: &str) -> Result { - let network = match chain { - Chain::Base => "base", - Chain::Ethereum => "ethereum", - _ => return Err(format!("unsupported chain for Yo API: {:?}", chain).into()), - }; - let url = format!("{}/api/v1/performance/user/{}/{}/{}", YO_API_BASE_URL, network, vault_address, user_address); - let target = Target::get(&url); - - let response = self - .rpc_provider - .request(target) - .await - .map_err(|e| format!("fetch performance error: request failed: {e}"))?; - - let parsed: YoApiResponse = - serde_json::from_slice(&response.data).map_err(|e| format!("fetch performance error: parse failed: {e}"))?; - - if parsed.status_code != 200 { - return Ok(YoPerformanceData::default()); - } - - Ok(parsed.data) - } -} diff --git a/crates/yielder/src/yo/api/mod.rs b/crates/yielder/src/yo/api/mod.rs deleted file mode 100644 index eb125d5ad..000000000 --- a/crates/yielder/src/yo/api/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod client; -mod model; - -pub use client::YoApiClient; -pub use model::YoPerformanceData; diff --git a/crates/yielder/src/yo/api/model.rs b/crates/yielder/src/yo/api/model.rs deleted file mode 100644 index 50c525642..000000000 --- a/crates/yielder/src/yo/api/model.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::Deserialize; -use serde_serializers::deserialize_u64_from_str_or_int; - -#[derive(Debug, Deserialize)] -pub struct YoApiResponse { - #[serde(default)] - pub data: T, - #[serde(rename = "statusCode")] - pub status_code: u32, -} - -#[derive(Debug, Default, Deserialize)] -pub struct YoPerformanceData { - #[serde(default)] - pub realized: YoFormattedValue, - #[serde(default)] - pub unrealized: YoFormattedValue, -} - -#[derive(Debug, Default, Deserialize)] -pub struct YoFormattedValue { - #[serde(default, deserialize_with = "deserialize_u64_from_str_or_int")] - pub raw: u64, - #[serde(default)] - pub formatted: String, -} - -impl YoPerformanceData { - pub fn total_rewards_raw(&self) -> u64 { - self.realized.raw.saturating_add(self.unrealized.raw) - } -} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index d2b43a635..36b229022 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -1,4 +1,3 @@ -mod api; mod client; mod contract; mod error; @@ -6,7 +5,6 @@ mod model; mod provider; mod vault; -pub use api::{YoApiClient, YoPerformanceData}; pub use client::{YoGatewayClient, YoProvider}; pub use contract::{IYoGateway, IYoVaultToken}; pub use error::{BoxError, YieldError}; diff --git a/crates/yielder/src/yo/model.rs b/crates/yielder/src/yo/model.rs index 219366748..1497a9402 100644 --- a/crates/yielder/src/yo/model.rs +++ b/crates/yielder/src/yo/model.rs @@ -1,4 +1,7 @@ use alloy_primitives::U256; +use gem_evm::u256::u256_to_f64; + +const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 60.0 * 60.0; /// Result from fetching position data via multicall #[derive(Debug, Clone)] @@ -10,3 +13,25 @@ pub struct PositionData { pub lookback_price: U256, pub lookback_timestamp: u64, } + +impl PositionData { + pub fn calculate_apy(&self) -> Option { + if self.lookback_price.is_zero() || self.lookback_timestamp >= self.latest_timestamp { + return None; + } + + let latest = u256_to_f64(self.latest_price); + let lookback = u256_to_f64(self.lookback_price); + let time_delta = (self.latest_timestamp - self.lookback_timestamp) as f64; + + if lookback == 0.0 || time_delta == 0.0 { + return None; + } + + let price_ratio = latest / lookback; + let periods_per_year = SECONDS_PER_YEAR / time_delta; + let apy = (price_ratio.powf(periods_per_year) - 1.0) * 100.0; + + if apy.is_finite() { Some(apy) } else { None } + } +} diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 1ec29d34b..3818b3902 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,19 +3,15 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; -use gem_jsonrpc::RpcProvider; use primitives::{AssetId, Chain, swap::ApprovalData}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; use crate::provider::YieldProviderClient; -use super::api::YoApiClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; pub const GAS_LIMIT: &str = "300000"; -const SECONDS_PER_YEAR: f64 = 31_536_000.0; - fn lookback_blocks_for_chain(chain: Chain) -> u64 { match chain { Chain::Base => 7 * 24 * 60 * 60 / 2, @@ -24,18 +20,16 @@ fn lookback_blocks_for_chain(chain: Chain) -> u64 { } } -pub struct YoYieldProvider { +pub struct YoYieldProvider { vaults: Vec, gateways: HashMap>, - api_client: YoApiClient, } -impl YoYieldProvider { - pub fn new(gateways: HashMap>, rpc_provider: Arc>) -> Self { +impl YoYieldProvider { + pub fn new(gateways: HashMap>) -> Self { Self { vaults: vaults().to_vec(), gateways, - api_client: YoApiClient::new(rpc_provider), } } @@ -51,10 +45,16 @@ impl YoYieldProvider { fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YieldError> { self.gateways.get(&chain).ok_or_else(|| format!("no gateway configured for chain {:?}", chain).into()) } + + async fn fetch_vault_apy(&self, vault: YoVault) -> Result { + let gateway = self.gateway_for_chain(vault.chain)?; + let data = gateway.get_position(vault, Address::ZERO, lookback_blocks_for_chain(vault.chain)).await?; + data.calculate_apy().ok_or_else(|| "failed to calculate apy".into()) + } } #[async_trait] -impl YieldProviderClient for YoYieldProvider { +impl YieldProviderClient for YoYieldProvider { fn provider(&self) -> YieldProvider { YieldProvider::Yo } @@ -67,16 +67,10 @@ impl YieldProviderClient for YoYie async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut results = Vec::new(); - for vault in self.vaults_for_asset(asset_id) { - let gateway = self.gateway_for_chain(vault.chain)?; - let lookback_blocks = lookback_blocks_for_chain(vault.chain); - let data = gateway.get_position(vault, Address::ZERO, lookback_blocks).await?; - let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); - let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); + let apy = self.fetch_vault_apy(vault).await.ok(); results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy, vault.risk)); } - Ok(results) } @@ -111,15 +105,6 @@ impl YieldProviderClient for YoYie let one_share = U256::from(10u64).pow(U256::from(vault.asset_decimals)); let asset_value = data.share_balance.saturating_mul(data.latest_price) / one_share; - let elapsed = data.latest_timestamp.saturating_sub(data.lookback_timestamp); - let apy = annualize_growth(data.latest_price, data.lookback_price, elapsed); - - let rewards = self - .api_client - .fetch_rewards(vault.chain, &vault.yo_token.to_string(), &request.wallet_address) - .await - .ok() - .map(|p| p.total_rewards_raw().to_string()); Ok(YieldPosition { name: vault.name.to_string(), @@ -129,8 +114,8 @@ impl YieldProviderClient for YoYie asset_token_address: vault.asset_token.to_string(), vault_balance_value: Some(data.share_balance.to_string()), asset_balance_value: Some(asset_value.to_string()), - apy, - rewards, + apy: None, + rewards: None, }) } } @@ -145,26 +130,3 @@ fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option Option { - if elapsed_seconds == 0 || previous_assets.is_zero() { - return None; - } - - let latest = u256_to_f64(latest_assets)?; - let previous = u256_to_f64(previous_assets)?; - if latest <= 0.0 || previous <= 0.0 { - return None; - } - - let growth = latest / previous; - if !growth.is_finite() || growth <= 0.0 { - return None; - } - - Some(growth.powf(SECONDS_PER_YEAR / elapsed_seconds as f64) - 1.0) -} - -fn u256_to_f64(value: U256) -> Option { - value.to_string().parse::().ok() -} diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index 4bda71c7d..5104263ff 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -4,9 +4,9 @@ use std::{collections::HashMap, sync::Arc}; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; -use gem_jsonrpc::{NativeProvider, RpcProvider}; +use gem_jsonrpc::NativeProvider; use primitives::{Chain, EVMChain}; -use yielder::{YO_GATEWAY, YO_USDC, YieldDetailsRequest, YieldProviderClient, Yielder, YoApiClient, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{YO_GATEWAY, YO_USDC, YieldDetailsRequest, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; fn get_endpoint(provider: &NativeProvider, chain: Chain) -> String { provider.get_endpoint(chain).unwrap_or_else(|err| panic!("missing RPC endpoint for chain {chain:?}: {err}")) @@ -23,51 +23,42 @@ fn build_gateways(provider: &NativeProvider) -> HashMap Arc { - Arc::new(NativeProvider::new().set_debug(false)) -} - #[tokio::test] -async fn test_yields_for_asset_with_apy() -> Result<(), Box> { - let rpc_provider = build_rpc_provider(); - let gateways = build_gateways(&rpc_provider); - let provider: Arc = Arc::new(YoYieldProvider::new(gateways, rpc_provider)); - let yielder = Yielder::with_providers(vec![provider]); - - let apy_yields = yielder.yields_for_asset_with_apy(&YO_USDC.asset_id()).await?; - println!("yielder: yields_for_asset_with_apy count={}", apy_yields.len()); - assert!(!apy_yields.is_empty(), "expected at least one Yo vault for asset"); - let apy = apy_yields[0].apy.expect("apy should be computed"); - println!("yielder: first Yo APY={}", apy); - assert!(apy.is_finite(), "apy should be finite"); - assert!(apy > -1.0, "apy should be > -100%"); +async fn test_yields_for_asset() -> Result<(), Box> { + let provider = NativeProvider::new().set_debug(false); + let gateways = build_gateways(&provider); + let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways)); + + let yields = yo_provider.yields(&YO_USDC.asset_id()); + println!("yielder: yields_for_asset count={}", yields.len()); + assert!(!yields.is_empty(), "expected at least one Yo vault for asset"); Ok(()) } #[tokio::test] -async fn test_yo_api_performance() -> Result<(), Box> { - let rpc_provider = build_rpc_provider(); - let api_client = YoApiClient::new(rpc_provider); - - let vault_address = YO_USDC.yo_token.to_string(); - let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; - - println!("yielder: fetch_rewards chain=Base vault={vault_address} wallet={wallet_address}"); +async fn test_yields_for_asset_with_apy() -> Result<(), Box> { + let provider = NativeProvider::new().set_debug(false); + let gateways = build_gateways(&provider); + let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways)); - let performance = api_client.fetch_rewards(Chain::Base, &vault_address, wallet_address).await?; + let yields = yo_provider.yields_with_apy(&YO_USDC.asset_id()).await?; + println!("yielder: yields_for_asset_with_apy count={}", yields.len()); + assert!(!yields.is_empty(), "expected at least one Yo vault for asset"); - println!("yielder: rewards total_raw={}", performance.total_rewards_raw(),); - assert!(performance.total_rewards_raw() > 0, "expected rewards for test address"); + let apy = yields[0].apy.expect("apy should be computed"); + println!("yielder: first Yo APY={:.2}%", apy); + assert!(apy.is_finite(), "apy should be finite"); + assert!(apy > -1.0, "apy should be > -100%"); Ok(()) } #[tokio::test] -async fn test_yo_positions_with_rewards() -> Result<(), Box> { - let rpc_provider = build_rpc_provider(); - let gateways = build_gateways(&rpc_provider); - let provider = YoYieldProvider::new(gateways, rpc_provider); +async fn test_yo_positions() -> Result<(), Box> { + let provider = NativeProvider::new().set_debug(false); + let gateways = build_gateways(&provider); + let yo_provider = YoYieldProvider::new(gateways); let wallet_address = "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7"; let request = YieldDetailsRequest { @@ -75,16 +66,14 @@ async fn test_yo_positions_with_rewards() -> Result<(), Box) -> Result = Arc::new(YoYieldProvider::new(gateways, wrapper)); + let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways)); Ok(Yielder::new(vec![yo_provider])) } From 9e01da998e1ba29c1b74012e4c725891d3932534 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:39:01 +0900 Subject: [PATCH 30/33] Fix value_to method call in ThorChain --- crates/swapper/src/thorchain/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/swapper/src/thorchain/mod.rs b/crates/swapper/src/thorchain/mod.rs index aa038800d..f462e0b9b 100644 --- a/crates/swapper/src/thorchain/mod.rs +++ b/crates/swapper/src/thorchain/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod model; mod provider; mod quote_data_mapper; +use bigint::value_to; use primitives::Chain; use std::sync::Arc; @@ -53,7 +54,7 @@ where fn map_quote_error(&self, error: SwapperError, decimals: i32) -> SwapperError { match error { SwapperError::InputAmountError { min_amount: Some(min) } => SwapperError::InputAmountError { - min_amount: Some(self.value_to(min, decimals).to_string()), + min_amount: value_to(&min, decimals).ok().map(|v| v.to_string()), }, other => other, } From 0564fbd251c2a012bc2c1ce37350b450ccb02188 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:45:47 +0900 Subject: [PATCH 31/33] fix merge error --- Cargo.lock | 1 + crates/swapper/Cargo.toml | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a88062f6..b0f795cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7562,6 +7562,7 @@ dependencies = [ "number_formatter", "primitives", "rand 0.9.2", + "reqwest 0.13.1", "serde", "serde_json", "serde_serializers", diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index 45e7f1a41..85575e62c 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -6,7 +6,8 @@ license = { workspace = true } [features] default = [] -swap_integration_tests = [] +reqwest_provider = ["dep:reqwest"] +swap_integration_tests = ["reqwest_provider"] [dependencies] primitives = { path = "../primitives" } @@ -17,12 +18,13 @@ gem_evm = { path = "../gem_evm", features = ["rpc"] } gem_sui = { path = "../gem_sui", features = ["rpc"] } gem_aptos = { path = "../gem_aptos", features = ["rpc"] } gem_hash = { path = "../gem_hash" } -gem_jsonrpc = { path = "../gem_jsonrpc", features = ["client"] } +gem_jsonrpc = { path = "../gem_jsonrpc" } gem_client = { path = "../gem_client" } gem_hypercore = { path = "../gem_hypercore" } serde_serializers = { path = "../serde_serializers" } number_formatter = { path = "../number_formatter" } +reqwest = { workspace = true, optional = true } typeshare = { version = "1.0.4" } strum = { workspace = true } @@ -48,4 +50,3 @@ tracing = "0.1.44" [dev-dependencies] tokio.workspace = true -gem_jsonrpc = { path = "../gem_jsonrpc", features = ["reqwest"] } From ad5b093bded19496133d157d5fe461dcf09bde5c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:53:05 +0900 Subject: [PATCH 32/33] fix lint --- crates/gem_evm/src/provider/preload_mapper.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index d7de95c36..72b70a52a 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -195,8 +195,10 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { - if earn_data.approval.is_some() && earn_data.gas_limit.is_some() { - Ok(BigInt::from_str_radix(earn_data.gas_limit.as_ref().unwrap(), 10)?) + if let Some(gas_limit) = earn_data.gas_limit.as_ref() + && earn_data.approval.is_some() + { + Ok(BigInt::from_str_radix(gas_limit, 10)?) } else { Ok(BigInt::from(0)) } From 994f8a48daf0a5a81b8d7f38993a2e8944b51a55 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:11:00 +0900 Subject: [PATCH 33/33] fix merge error --- bin/gas-bench/Cargo.toml | 1 - bin/gas-bench/src/solana_client.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/gas-bench/Cargo.toml b/bin/gas-bench/Cargo.toml index 8383910ea..7239e56ad 100644 --- a/bin/gas-bench/Cargo.toml +++ b/bin/gas-bench/Cargo.toml @@ -14,7 +14,6 @@ serde_json = { workspace = true } prettytable-rs = "^0.10" primitives = { path = "../../crates/primitives" } -gem_jsonrpc = { path = "../../crates/gem_jsonrpc" } gemstone = { path = "../../gemstone", features = ["reqwest_provider"] } gem_evm = { path = "../../crates/gem_evm" } gem_solana = { path = "../../crates/gem_solana", features = ["reqwest"] } diff --git a/bin/gas-bench/src/solana_client.rs b/bin/gas-bench/src/solana_client.rs index 4ed2e9cac..682ba8cb8 100644 --- a/bin/gas-bench/src/solana_client.rs +++ b/bin/gas-bench/src/solana_client.rs @@ -1,10 +1,10 @@ use std::error::Error; use std::sync::Arc; -use gem_jsonrpc::client::JsonRpcClient; +use gem_jsonrpc::{NativeProvider, client::JsonRpcClient}; use gem_solana::models::jito::{FeeStats, JitoTipEstimates, calculate_fee_stats, estimate_jito_tips}; use gem_solana::models::prioritization_fee::SolanaPrioritizationFee; -use gemstone::alien::{AlienProvider, new_alien_client, reqwest_provider::NativeProvider}; +use gemstone::alien::{AlienProvider, new_alien_client}; use primitives::Chain; use serde_json::json;