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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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; From 4bc869f1b43de767c6c98de65bc597e788e44ca6 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:46:04 +0900 Subject: [PATCH 34/43] remove GemRiskLevel and rename banner.yield --- crates/primitives/src/banner.rs | 2 +- crates/yielder/src/lib.rs | 2 +- crates/yielder/src/models.rs | 12 +----------- crates/yielder/src/provider.rs | 2 +- crates/yielder/src/yo/provider.rs | 4 ++-- crates/yielder/src/yo/vault.rs | 8 +------- gemstone/src/gem_yielder/remote_types.rs | 12 +----------- 7 files changed, 8 insertions(+), 34 deletions(-) diff --git a/crates/primitives/src/banner.rs b/crates/primitives/src/banner.rs index 684a7b5d5..aca9360e7 100644 --- a/crates/primitives/src/banner.rs +++ b/crates/primitives/src/banner.rs @@ -19,7 +19,7 @@ pub enum BannerEvent { SuspiciousAsset, Onboarding, TradePerpetuals, - Yield, + Earn, } #[typeshare(swift = "Equatable, CaseIterable, Sendable")] diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 7a996350d..9ffa6f001 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::{RiskLevel, Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +pub use models::{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, YoGatewayClient, YoProvider, YoVault, YoYieldProvider, vaults, diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index 06baf12a0..81a93fe7d 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -8,31 +8,21 @@ 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, risk: RiskLevel) -> Self { + pub fn new(name: impl Into, asset_id: AssetId, provider: YieldProvider, apy: Option) -> 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 a4d6205bb..b051b0fdf 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -38,7 +38,7 @@ impl Yielder { } 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)) + apy_cmp.then_with(|| a.name.cmp(&b.name)) }); Ok(yields) } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 3818b3902..5b01e5b68 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -61,7 +61,7 @@ impl YieldProviderClient for YoYieldProvider { 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, vault.risk)) + .map(|vault| Yield::new(vault.name, vault.asset_id(), self.provider(), None)) .collect() } @@ -69,7 +69,7 @@ impl YieldProviderClient for YoYieldProvider { let mut results = Vec::new(); for vault in self.vaults_for_asset(asset_id) { let apy = self.fetch_vault_apy(vault).await.ok(); - results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy, vault.risk)); + results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); } Ok(results) } diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs index 58b9a0a61..57f790692 100644 --- a/crates/yielder/src/yo/vault.rs +++ b/crates/yielder/src/yo/vault.rs @@ -1,8 +1,6 @@ use alloy_primitives::{Address, address}; use primitives::{AssetId, Chain}; -use crate::models::RiskLevel; - #[derive(Debug, Clone, Copy)] pub struct YoVault { pub name: &'static str, @@ -10,18 +8,16 @@ 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, risk: RiskLevel) -> Self { + 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, - risk, } } @@ -36,7 +32,6 @@ pub const YO_USDC: YoVault = YoVault::new( address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), 6, - RiskLevel::Medium, ); pub const YO_USDT: YoVault = YoVault::new( @@ -45,7 +40,6 @@ 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 dfc89b651..d75a4a375 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::{RiskLevel, Yield, YieldPosition, YieldProvider, YieldTransaction}; +use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; use crate::models::swap::GemApprovalData; pub use crate::models::transaction::GemYieldAction; @@ -11,15 +11,6 @@ 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, @@ -36,7 +27,6 @@ pub struct GemYield { pub asset_id: AssetId, pub provider: GemYieldProvider, pub apy: Option, - pub risk: GemRiskLevel, } pub type GemYieldTransaction = YieldTransaction; From 5b1983a9a81a6cd56b2bccc82fdd4506a95940bc Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:11:38 +0900 Subject: [PATCH 35/43] Rename Yield -> Earn and add earn primitives Replace the legacy Yield types with Earn equivalents across the codebase: TransactionInputType::Yield -> TransactionInputType::Earn and YieldAction -> EarnAction. Add new primitives (earn_action, earn_data, earn_position, earn_provider) and remove the old yield_data. Update providers, preload mappers, RPC clients, yielder implementation and tests to use the new Earn types and BigInt values where appropriate. Adjust Uniffi remote types in gemstone to GemEarn* and use GemBigInt for position balances. Add num-bigint dependency to Cargo and yielder Cargo.toml. Misc: small formatting/unwrap fixes in yielder client code and gas/fee mappings updated to reference Earn variants. --- Cargo.lock | 1 + crates/gem_aptos/src/rpc/client.rs | 2 +- .../gem_cosmos/src/provider/preload_mapper.rs | 12 ++--- crates/gem_evm/src/provider/preload.rs | 2 +- crates/gem_evm/src/provider/preload_mapper.rs | 10 ++--- .../gem_solana/src/provider/preload_mapper.rs | 4 +- crates/gem_sui/src/provider/preload_mapper.rs | 2 +- crates/primitives/src/earn_action.rs | 9 ++++ crates/primitives/src/earn_data.rs | 28 ++++++++++++ crates/primitives/src/earn_position.rs | 22 ++++++++++ crates/primitives/src/earn_provider.rs | 11 +++++ crates/primitives/src/lib.rs | 10 ++++- .../primitives/src/transaction_input_type.rs | 15 ++++--- .../src/transaction_load_metadata.rs | 2 +- crates/primitives/src/yield_data.rs | 44 ------------------- crates/yielder/Cargo.toml | 1 + crates/yielder/src/models.rs | 40 ++--------------- crates/yielder/src/yo/client.rs | 9 +--- crates/yielder/src/yo/provider.rs | 11 ++++- crates/yielder/tests/integration_test.rs | 7 +-- gemstone/src/gem_yielder/mod.rs | 26 +++++------ gemstone/src/gem_yielder/remote_types.rs | 24 +++++----- gemstone/src/models/mod.rs | 1 + gemstone/src/models/transaction.rs | 17 ++++--- 24 files changed, 156 insertions(+), 154 deletions(-) create mode 100644 crates/primitives/src/earn_action.rs create mode 100644 crates/primitives/src/earn_data.rs create mode 100644 crates/primitives/src/earn_position.rs create mode 100644 crates/primitives/src/earn_provider.rs delete mode 100644 crates/primitives/src/yield_data.rs diff --git a/Cargo.lock b/Cargo.lock index a90ed3a86..c07391d11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9222,6 +9222,7 @@ dependencies = [ "gem_client", "gem_evm", "gem_jsonrpc", + "num-bigint", "primitives", "reqwest 0.13.1", "serde", diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index 651bafabc..a7a096b63 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -111,7 +111,7 @@ impl AptosClient { | TransactionInputType::Stake(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Yield(_, _, _) => Ok(1500), + | TransactionInputType::Earn(_, _, _) => 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 db0d467cb..d4c503e72 100644 --- a/crates/gem_cosmos/src/provider/preload_mapper.rs +++ b/crates/gem_cosmos/src/provider/preload_mapper.rs @@ -12,7 +12,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(3_000u64), + | TransactionInputType::Earn(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(25_000u64), }, @@ -24,7 +24,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(10_000u64), + | TransactionInputType::Earn(_, _, _) => BigInt::from(10_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(10_000u64), TransactionInputType::Stake(_, _) => BigInt::from(100_000u64), }, @@ -36,7 +36,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(3_000u64), + | TransactionInputType::Earn(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(10_000u64), }, @@ -48,7 +48,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000u64), + | TransactionInputType::Earn(_, _, _) => BigInt::from(100_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(100_000u64), TransactionInputType::Stake(_, _) => BigInt::from(200_000u64), }, @@ -60,7 +60,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000_000_000_000u64), + | TransactionInputType::Earn(_, _, _) => 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), }, @@ -77,7 +77,7 @@ fn get_gas_limit(input_type: &TransactionInputType, _chain: CosmosChain) -> u64 | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => 200_000, + | TransactionInputType::Earn(_, _, _) => 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 5189fd34f..287165bc8 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -70,7 +70,7 @@ impl EthereumClient { }, _ => input.metadata, }, - TransactionInputType::Yield(_, _, earn_input) => match input.metadata { + TransactionInputType::Earn(_, _, earn_input) => match input.metadata { TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { nonce, chain_id, diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index 72b70a52a..8f2f9197b 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -8,7 +8,7 @@ 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, + AssetSubtype, Chain, EVMChain, EarnAction, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, fee::FeePriority, fee::GasPriceType, }; @@ -142,7 +142,7 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> } _ => Err("Unsupported chain for staking".into()), }, - TransactionInputType::Yield(_, action, earn_data) => { + TransactionInputType::Earn(_, 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 { @@ -150,8 +150,8 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> 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), + EarnAction::Deposit => BigInt::from(0), + EarnAction::Withdraw => BigInt::from(0), }; Ok(TransactionParams::new(contract_address.clone(), decoded_data, tx_value)) } @@ -194,7 +194,7 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { + TransactionInputType::Earn(_, _, earn_data) => { if let Some(gas_limit) = earn_data.gas_limit.as_ref() && earn_data.approval.is_some() { diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 56b6aff42..dc42feefa 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -42,7 +42,7 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => BigInt::from(100_000), + | TransactionInputType::Earn(_, _, _) => BigInt::from(100_000), TransactionInputType::Swap(_, _, _) => BigInt::from(420_000), TransactionInputType::Stake(_, _) => BigInt::from(100_000), } @@ -57,7 +57,7 @@ fn get_multiple_of(input_type: &TransactionInputType) -> i64 { | TransactionInputType::TokenApprove(asset, _) | TransactionInputType::Generic(asset, _, _) | TransactionInputType::Perpetual(asset, _) - | TransactionInputType::Yield(asset, _, _) => match &asset.id.token_subtype() { + | TransactionInputType::Earn(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 931565464..e397570c9 100644 --- a/crates/gem_sui/src/provider/preload_mapper.rs +++ b/crates/gem_sui/src/provider/preload_mapper.rs @@ -37,7 +37,7 @@ fn get_gas_limit(input_type: &TransactionInputType) -> u64 { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Yield(_, _, _) => GAS_BUDGET, + | TransactionInputType::Earn(_, _, _) => GAS_BUDGET, TransactionInputType::Swap(_, _, _) => 50_000_000, TransactionInputType::Stake(_, _) => GAS_BUDGET, } diff --git a/crates/primitives/src/earn_action.rs b/crates/primitives/src/earn_action.rs new file mode 100644 index 000000000..6051fafcd --- /dev/null +++ b/crates/primitives/src/earn_action.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +pub enum EarnAction { + Deposit, + Withdraw, +} diff --git a/crates/primitives/src/earn_data.rs b/crates/primitives/src/earn_data.rs new file mode 100644 index 000000000..c162ea62a --- /dev/null +++ b/crates/primitives/src/earn_data.rs @@ -0,0 +1,28 @@ +use hex::encode; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::swap::ApprovalData; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +#[serde(rename_all = "camelCase")] +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: Some(encode(call_data)), + approval: None, + gas_limit: None, + } + } +} diff --git a/crates/primitives/src/earn_position.rs b/crates/primitives/src/earn_position.rs new file mode 100644 index 000000000..631ae4e43 --- /dev/null +++ b/crates/primitives/src/earn_position.rs @@ -0,0 +1,22 @@ +use num_bigint::BigInt; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::AssetId; +use crate::earn_provider::EarnProvider; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +#[serde(rename_all = "camelCase")] +pub struct EarnPosition { + pub asset_id: AssetId, + pub provider: EarnProvider, + pub name: String, + pub vault_token_address: String, + pub asset_token_address: String, + pub vault_balance_value: BigInt, + pub asset_balance_value: BigInt, + pub balance: String, + pub rewards: Option, + pub apy: Option, +} diff --git a/crates/primitives/src/earn_provider.rs b/crates/primitives/src/earn_provider.rs new file mode 100644 index 000000000..51e3ac7d2 --- /dev/null +++ b/crates/primitives/src/earn_provider.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString}; +use typeshare::typeshare; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq, Eq)] +#[typeshare(swift = "Equatable, CaseIterable, Sendable")] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum EarnProvider { + Yo, +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index fa981a317..dbe40ced9 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -215,6 +215,10 @@ pub mod chart; pub use self::chart::{ChartCandleStick, ChartDateValue}; pub mod delegation; pub use self::delegation::{Delegation, DelegationBase, DelegationState, DelegationValidator}; +pub mod earn_provider; +pub use self::earn_provider::EarnProvider; +pub mod earn_position; +pub use self::earn_position::EarnPosition; pub mod transaction_update; pub use self::transaction_update::{TransactionChange, TransactionMetadata, TransactionStateRequest, TransactionUpdate}; pub mod transaction_preload_input; @@ -229,8 +233,10 @@ 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::{EarnData, YieldAction}; +pub mod earn_action; +pub use self::earn_action::EarnAction; +pub mod earn_data; +pub use self::earn_data::EarnData; 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 75f9606da..5a5b0f53e 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -1,8 +1,9 @@ +use crate::earn_action::EarnAction; +use crate::earn_data::EarnData; 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::{EarnData, YieldAction}; use crate::{ Asset, GasPriceType, PerpetualType, TransactionPreloadInput, TransactionType, TransferDataExtra, WalletConnectionSessionAppMetadata, nft::NFTAsset, perpetual::AccountDataType, }; @@ -22,7 +23,7 @@ pub enum TransactionInputType { TransferNft(Asset, NFTAsset), Account(Asset, AccountDataType), Perpetual(Asset, PerpetualType), - Yield(Asset, YieldAction, EarnData), + Earn(Asset, EarnAction, EarnData), } impl TransactionInputType { @@ -37,7 +38,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, - TransactionInputType::Yield(asset, _, _) => asset, + TransactionInputType::Earn(asset, _, _) => asset, } } @@ -52,7 +53,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, - TransactionInputType::Yield(asset, _, _) => asset, + TransactionInputType::Earn(asset, _, _) => asset, } } @@ -77,9 +78,9 @@ impl TransactionInputType { PerpetualType::Close(_) | PerpetualType::Reduce(_) => TransactionType::PerpetualClosePosition, PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, - TransactionInputType::Yield(_, action, _) => match action { - YieldAction::Deposit => TransactionType::EarnDeposit, - YieldAction::Withdraw => TransactionType::EarnWithdraw, + TransactionInputType::Earn(_, action, _) => match action { + EarnAction::Deposit => TransactionType::EarnDeposit, + EarnAction::Withdraw => TransactionType::EarnWithdraw, }, } } diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index 17a250c42..613992d3a 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, yield_data::EarnData}; +use crate::{UTXO, earn_data::EarnData, solana_token_program::SolanaTokenProgramId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HyperliquidOrder { diff --git a/crates/primitives/src/yield_data.rs b/crates/primitives/src/yield_data.rs deleted file mode 100644 index a334cb3e4..000000000 --- a/crates/primitives/src/yield_data.rs +++ /dev/null @@ -1,44 +0,0 @@ -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 { - Deposit, - Withdraw, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[typeshare(swift = "Equatable, Hashable, Sendable")] -#[serde(rename_all = "camelCase")] -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/Cargo.toml b/crates/yielder/Cargo.toml index 0b14442c1..166b927ee 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -15,6 +15,7 @@ yield_integration_tests = [ [dependencies] alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } +num-bigint = { workspace = true } gem_client = { path = "../gem_client" } gem_evm = { path = "../gem_evm", features = ["rpc"] } gem_jsonrpc = { path = "../gem_jsonrpc" } diff --git a/crates/yielder/src/models.rs b/crates/yielder/src/models.rs index 81a93fe7d..1381ee783 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -1,12 +1,7 @@ -use alloy_primitives::Address; -use primitives::{AssetId, Chain, swap::ApprovalData}; -use strum::{AsRefStr, Display, EnumString}; +use primitives::{AssetId, Chain, EarnPosition, EarnProvider, swap::ApprovalData}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr)] -#[strum(serialize_all = "lowercase")] -pub enum YieldProvider { - Yo, -} +pub type YieldProvider = EarnProvider; +pub type YieldPosition = EarnPosition; #[derive(Debug, Clone)] pub struct Yield { @@ -42,32 +37,3 @@ 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/yo/client.rs b/crates/yielder/src/yo/client.rs index a49c5ce24..8e1343e44 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -92,11 +92,7 @@ where } async fn get_position(&self, vault: YoVault, owner: Address, lookback_blocks: u64) -> Result { - let latest_block = self - .ethereum_client - .get_latest_block() - .await - .map_err(|e| format!("failed to fetch latest block: {e}"))?; + let latest_block = self.ethereum_client.get_latest_block().await.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)); @@ -158,8 +154,7 @@ where .await .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}"))?; + 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/provider.rs b/crates/yielder/src/yo/provider.rs index 5b01e5b68..eb61b33c6 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,6 +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 num_bigint::BigInt; use primitives::{AssetId, Chain, swap::ApprovalData}; use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; @@ -106,14 +107,20 @@ impl YieldProviderClient for YoYieldProvider { 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 asset_value_string = asset_value.to_string(); + let asset_value_value = BigInt::from_str(&asset_value_string) + .map_err(|e| format!("invalid asset value {asset_value_string}: {e}"))?; + let share_balance_value = BigInt::from_str(&data.share_balance.to_string()) + .map_err(|e| format!("invalid share balance {}: {e}", data.share_balance))?; 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()), + vault_balance_value: share_balance_value, + asset_balance_value: asset_value_value, + balance: asset_value_string, apy: None, rewards: None, }) diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs index 5104263ff..57380ffa1 100644 --- a/crates/yielder/tests/integration_test.rs +++ b/crates/yielder/tests/integration_test.rs @@ -3,8 +3,9 @@ use std::{collections::HashMap, sync::Arc}; use gem_evm::rpc::EthereumClient; -use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::NativeProvider; +use gem_jsonrpc::client::JsonRpcClient; +use num_bigint::BigInt; use primitives::{Chain, EVMChain}; use yielder::{YO_GATEWAY, YO_USDC, YieldDetailsRequest, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; @@ -72,8 +73,8 @@ async fn test_yo_positions() -> Result<(), Box= BigInt::from(0)); + assert!(position.asset_balance_value >= BigInt::from(0)); Ok(()) } diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 2ec11bbab..8ac186080 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, GAS_LIMIT}; +use yielder::{GAS_LIMIT, YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, YieldTransaction, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -31,33 +31,29 @@ impl GemYielder { 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, value: String) -> Result { - let provider = provider.parse::()?; + pub async fn deposit(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String, value: String) -> Result { 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, value: String) -> Result { - let provider = provider.parse::()?; + pub async fn withdraw(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String, value: String) -> Result { self.yielder.withdraw(provider, &asset, &wallet_address, &value).await.map_err(Into::into) } - pub async fn positions(&self, provider: String, asset: AssetId, wallet_address: String) -> Result { - let provider = provider.parse::()?; + pub async fn positions(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String) -> Result { let request = YieldDetailsRequest { asset_id: asset, wallet_address }; self.yielder.positions(provider, &request).await.map_err(Into::into) } pub async fn build_transaction( &self, - action: GemYieldAction, - provider: String, + action: GemEarnAction, + provider: GemEarnProvider, asset: AssetId, wallet_address: String, value: String, nonce: u64, chain_id: u64, ) -> Result { - let provider = provider.parse::()?; let transaction = build_yield_transaction(&self.yielder, &action, provider, &asset, &wallet_address, &value).await?; Ok(GemYieldTransactionData { @@ -90,12 +86,12 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result Result { match &input.input_type { - GemTransactionInputType::Yield { asset, action, data } => { + GemTransactionInputType::Earn { asset, action, data } => { 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 { + input_type: GemTransactionInputType::Earn { asset: asset.clone(), action: action.clone(), data: GemEarnData { @@ -124,14 +120,14 @@ pub(crate) async fn prepare_yield_input(yielder: &Yielder, input: GemTransaction async fn build_yield_transaction( yielder: &Yielder, - action: &GemYieldAction, + action: &GemEarnAction, 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?), + GemEarnAction::Deposit => Ok(yielder.deposit(provider, asset, wallet_address, value).await?), + GemEarnAction::Withdraw => Ok(yielder.withdraw(provider, asset, wallet_address, value).await?), } } diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index d75a4a375..9c18d238c 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,13 +1,14 @@ -use primitives::AssetId; -use yielder::{Yield, YieldPosition, YieldProvider, YieldTransaction}; +use primitives::{AssetId, EarnPosition, EarnProvider}; +use yielder::{Yield, YieldTransaction}; use crate::models::swap::GemApprovalData; -pub use crate::models::transaction::GemYieldAction; +use crate::models::GemBigInt; +pub use crate::models::transaction::GemEarnAction; -pub type GemYieldProvider = YieldProvider; +pub type GemEarnProvider = EarnProvider; #[uniffi::remote(Enum)] -pub enum GemYieldProvider { +pub enum GemEarnProvider { Yo, } @@ -25,7 +26,7 @@ pub type GemYield = Yield; pub struct GemYield { pub name: String, pub asset_id: AssetId, - pub provider: GemYieldProvider, + pub provider: GemEarnProvider, pub apy: Option, } @@ -41,17 +42,18 @@ pub struct GemYieldTransaction { pub approval: Option, } -pub type GemYieldPosition = YieldPosition; +pub type GemEarnPosition = EarnPosition; #[uniffi::remote(Record)] -pub struct GemYieldPosition { +pub struct GemEarnPosition { pub name: String, pub asset_id: AssetId, - pub provider: GemYieldProvider, + pub provider: GemEarnProvider, pub vault_token_address: String, pub asset_token_address: String, - pub vault_balance_value: Option, - pub asset_balance_value: Option, + pub vault_balance_value: GemBigInt, + pub asset_balance_value: GemBigInt, + pub balance: String, pub apy: Option, pub rewards: Option, } diff --git a/gemstone/src/models/mod.rs b/gemstone/src/models/mod.rs index 347d13a21..0d5f5e01d 100644 --- a/gemstone/src/models/mod.rs +++ b/gemstone/src/models/mod.rs @@ -16,6 +16,7 @@ pub mod transaction; pub use address::*; pub use asset::*; pub use balance::*; +pub use custom_types::{DateTimeUtc, GemBigInt, GemBigUint}; pub use gateway::*; pub use nft::*; pub use node::*; diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 81df24c97..9807bc887 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -2,10 +2,9 @@ use crate::models::*; use num_bigint::BigInt; use primitives::stake_type::FreezeData; use primitives::{ - AccountDataType, Asset, EarnData, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, StakeType, + AccountDataType, Asset, EarnAction, 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, perpetual::{CancelOrderData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, TPSLOrderData}, }; use std::collections::HashMap; @@ -236,10 +235,10 @@ pub enum PerpetualType { Reduce(PerpetualReduceData), } -pub type GemYieldAction = YieldAction; +pub type GemEarnAction = EarnAction; #[uniffi::remote(Enum)] -pub enum YieldAction { +pub enum EarnAction { Deposit, Withdraw, } @@ -294,9 +293,9 @@ pub enum GemTransactionInputType { asset: GemAsset, perpetual_type: GemPerpetualType, }, - Yield { + Earn { asset: GemAsset, - action: GemYieldAction, + action: GemEarnAction, data: GemEarnData, }, } @@ -312,7 +311,7 @@ impl GemTransactionInputType { | Self::TransferNft { asset, .. } | Self::Account { asset, .. } | Self::Perpetual { asset, .. } - | Self::Yield { asset, .. } => asset, + | Self::Earn { asset, .. } => asset, Self::Swap { from_asset, .. } => from_asset, } } @@ -678,7 +677,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, data }, + TransactionInputType::Earn(asset, action, data) => GemTransactionInputType::Earn { asset, action, data }, } } } @@ -832,7 +831,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, data), + GemTransactionInputType::Earn { asset, action, data } => TransactionInputType::Earn(asset, action, data), } } } From 950d8919c5b838d2955a4bd745636e1c525cbd22 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:46:35 +0900 Subject: [PATCH 36/43] remove not used name --- apps/daemon/src/worker/assets/mod.rs | 1 - crates/primitives/src/earn_position.rs | 1 - crates/yielder/src/yo/provider.rs | 7 ++----- gemstone/src/gem_yielder/remote_types.rs | 3 +-- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/daemon/src/worker/assets/mod.rs b/apps/daemon/src/worker/assets/mod.rs index 9baf02c68..6dc6b4884 100644 --- a/apps/daemon/src/worker/assets/mod.rs +++ b/apps/daemon/src/worker/assets/mod.rs @@ -121,7 +121,6 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Date: Thu, 5 Feb 2026 00:14:21 +0900 Subject: [PATCH 37/43] Update balance_type.rs --- crates/primitives/src/balance_type.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/primitives/src/balance_type.rs b/crates/primitives/src/balance_type.rs index f3d501896..e45394766 100644 --- a/crates/primitives/src/balance_type.rs +++ b/crates/primitives/src/balance_type.rs @@ -8,4 +8,5 @@ pub enum BalanceType { pendingUnconfirmed, rewards, reserved, + earn, } From e72f4fe2db7bf5c00ff6aedab14d9c5214a7e757 Mon Sep 17 00:00:00 2001 From: gemdev1111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:10:27 +0200 Subject: [PATCH 38/43] Yielder earn (#937) --- Cargo.lock | 121 ++--- Cargo.toml | 16 +- Settings.yaml | 5 +- apps/api/src/assets/cilent.rs | 10 +- apps/api/src/assets/mod.rs | 30 +- apps/api/src/auth/guard.rs | 6 - apps/api/src/catchers.rs | 4 + apps/api/src/devices/mod.rs | 12 +- apps/api/src/fiat/client.rs | 8 +- apps/api/src/fiat/mod.rs | 6 +- apps/api/src/main.rs | 23 +- apps/api/src/metrics/client.rs | 2 +- apps/api/src/metrics/consumer.rs | 27 +- apps/api/src/metrics/fiat.rs | 41 ++ apps/api/src/metrics/job.rs | 29 +- apps/api/src/metrics/parser.rs | 20 +- apps/api/src/metrics/price.rs | 66 ++- apps/api/src/nft/mod.rs | 18 +- apps/api/src/subscriptions/client.rs | 36 -- apps/api/src/subscriptions/mod.rs | 29 -- apps/api/src/support/client.rs | 22 - apps/api/src/support/mod.rs | 30 -- apps/api/src/transactions/client.rs | 36 +- apps/api/src/transactions/mod.rs | 37 +- apps/api/src/websocket_stream/client.rs | 2 + apps/daemon/Cargo.toml | 1 + .../consumers/assets_addresses_consumer.rs | 32 -- .../daemon/src/consumers/consumer_reporter.rs | 71 --- .../fiat/fiat_webhook_consumer.rs | 0 apps/daemon/src/consumers/fiat/mod.rs | 21 + .../fetch_address_transactions_consumer.rs | 0 .../{ => indexer}/fetch_assets_consumer.rs | 0 .../{ => indexer}/fetch_blocks_consumer.rs | 0 .../fetch_coin_addresses_consumer.rs | 0 .../fetch_nft_assets_addresses_consumer.rs | 5 +- .../fetch_token_addresses_consumer.rs | 0 apps/daemon/src/consumers/indexer/mod.rs | 181 +++++++ apps/daemon/src/consumers/mod.rs | 471 +----------------- .../src/consumers/nft/collections_updater.rs | 85 ---- apps/daemon/src/consumers/nft/mod.rs | 72 --- .../nft/nft_collection_asset_consumer.rs | 22 - .../consumers/nft/nft_collection_consumer.rs | 22 - .../daemon/src/consumers/notifications/mod.rs | 34 +- .../{ => prices}/fetch_prices_consumer.rs | 2 +- apps/daemon/src/consumers/prices/mod.rs | 37 ++ apps/daemon/src/consumers/rewards/mod.rs | 98 ++++ .../{ => rewards}/rewards_consumer.rs | 0 .../rewards_redemption_consumer.rs | 0 apps/daemon/src/consumers/runner.rs | 85 ++++ apps/daemon/src/consumers/store/mod.rs | 90 ++++ .../{ => store}/store_charts_consumer.rs | 0 .../{ => store}/store_prices_consumer.rs | 0 .../store_transactions_consumer.rs | 28 +- .../store_transactions_consumer_config.rs | 0 apps/daemon/src/consumers/support/mod.rs | 18 + .../support/support_webhook_consumer.rs | 28 +- apps/daemon/src/health.rs | 46 ++ apps/daemon/src/main.rs | 216 +++++--- apps/daemon/src/model.rs | 65 +-- apps/daemon/src/parser/mod.rs | 263 +++++----- apps/daemon/src/parser/parser_options.rs | 4 +- apps/daemon/src/parser/parser_state.rs | 62 +-- apps/daemon/src/parser/plan.rs | 159 ++++++ apps/daemon/src/pusher/pusher.rs | 27 +- apps/daemon/src/reporters/consumer.rs | 47 ++ apps/daemon/src/reporters/mod.rs | 21 + apps/daemon/src/reporters/parser.rs | 23 + apps/daemon/src/setup/mod.rs | 101 ++-- apps/daemon/src/shutdown.rs | 10 +- apps/daemon/src/worker/alerter/mod.rs | 3 +- apps/daemon/src/worker/assets/mod.rs | 93 ++-- .../{scan => assets}/validator_scanner.rs | 0 apps/daemon/src/worker/fiat/mod.rs | 55 +- apps/daemon/src/worker/job_history.rs | 59 --- apps/daemon/src/worker/job_reporter.rs | 49 -- apps/daemon/src/worker/job_schedule.rs | 80 +++ apps/daemon/src/worker/jobs.rs | 127 +++-- apps/daemon/src/worker/mod.rs | 31 +- apps/daemon/src/worker/plan.rs | 40 +- .../{pricer => prices}/charts_updater.rs | 13 +- .../{pricer => prices}/markets_updater.rs | 0 .../src/worker/{pricer => prices}/mod.rs | 112 ++++- .../observed_prices_updater.rs | 0 .../{pricer => prices}/price_updater.rs | 4 +- .../prices_dex_updater.rs | 0 apps/daemon/src/worker/prices_dex/mod.rs | 67 --- apps/daemon/src/worker/rewards/mod.rs | 3 +- apps/daemon/src/worker/scan/mod.rs | 64 --- .../{device => system}/device_updater.rs | 0 .../src/worker/{device => system}/mod.rs | 26 +- .../src/worker/{version => system}/model.rs | 0 .../observers/inactive_devices_observer.rs | 5 +- .../{device => system}/observers/mod.rs | 0 .../transaction_updater.rs | 4 +- .../src/worker/system/version_updater.rs | 73 +++ apps/daemon/src/worker/transaction/mod.rs | 27 - apps/daemon/src/worker/version/mod.rs | 28 -- .../src/worker/version/version_updater.rs | 86 ---- crates/cacher/src/keys.rs | 7 - crates/cacher/src/lib.rs | 18 +- crates/fiat/src/client.rs | 12 +- .../gem_aptos/src/provider/staking_mapper.rs | 3 +- crates/gem_aptos/src/rpc/client.rs | 2 +- crates/gem_client/src/types.rs | 20 +- .../gem_cosmos/src/provider/preload_mapper.rs | 12 +- .../gem_cosmos/src/provider/staking_mapper.rs | 3 +- crates/gem_evm/src/provider/preload.rs | 9 +- crates/gem_evm/src/provider/preload_mapper.rs | 34 +- .../gem_evm/src/provider/staking_ethereum.rs | 3 +- crates/gem_evm/src/provider/staking_monad.rs | 3 +- .../src/provider/staking_smartchain.rs | 3 +- .../gem_hypercore/src/models/candlestick.rs | 30 +- crates/gem_hypercore/src/models/websocket.rs | 4 +- crates/gem_hypercore/src/provider/balances.rs | 8 +- .../src/provider/perpetual_mapper.rs | 4 +- .../src/provider/staking_mapper.rs | 3 +- .../src/provider/websocket_mapper.rs | 15 +- .../gem_hypercore/src/signer/core_signer.rs | 6 +- crates/gem_jsonrpc/src/types.rs | 72 ++- crates/gem_near/src/provider/balances.rs | 15 +- .../gem_solana/src/provider/preload_mapper.rs | 4 +- .../gem_solana/src/provider/staking_mapper.rs | 3 +- crates/gem_sui/src/provider/preload_mapper.rs | 2 +- crates/gem_sui/src/provider/staking_mapper.rs | 3 +- crates/gem_tron/Cargo.toml | 2 + crates/gem_tron/src/provider/preload.rs | 55 +- .../gem_tron/src/provider/preload_mapper.rs | 67 ++- .../gem_tron/src/provider/staking_mapper.rs | 4 +- crates/job_runner/src/lib.rs | 27 +- crates/name_resolver/Cargo.toml | 2 +- crates/nft/src/client.rs | 36 +- crates/nft/src/lib.rs | 2 +- crates/nft/src/provider.rs | 67 +-- .../src/providers/magiceden/evm/provider.rs | 6 +- .../providers/magiceden/solana/provider.rs | 6 +- crates/nft/src/providers/opensea/provider.rs | 6 +- crates/pricer/src/price_client.rs | 14 +- crates/primitives/src/asset_details.rs | 15 + crates/primitives/src/asset_price.rs | 26 + crates/primitives/src/asset_price_info.rs | 11 +- crates/primitives/src/chain_nft.rs | 33 ++ crates/primitives/src/chart.rs | 10 +- crates/primitives/src/config_key.rs | 20 +- crates/primitives/src/consumer_status.rs | 18 - crates/primitives/src/delegation.rs | 2 + crates/primitives/src/earn_action.rs | 9 - crates/primitives/src/earn_position.rs | 21 - crates/primitives/src/earn_type.rs | 37 ++ .../{earn_provider.rs => growth_provider.rs} | 11 +- crates/primitives/src/job_status.rs | 10 - crates/primitives/src/lib.rs | 29 +- crates/primitives/src/metrics.rs | 33 ++ crates/primitives/src/parser_status.rs | 13 - crates/primitives/src/price.rs | 13 +- crates/primitives/src/push_notification.rs | 1 - crates/primitives/src/stake_type.rs | 33 +- crates/primitives/src/stream.rs | 11 + crates/primitives/src/subscription.rs | 11 +- crates/primitives/src/support.rs | 25 - .../primitives/src/testkit/delegation_mock.rs | 3 +- .../primitives/src/transaction_input_type.rs | 15 +- .../src/transaction_load_metadata.rs | 6 +- crates/settings/src/lib.rs | 14 +- crates/storage/src/database/charts.rs | 37 +- crates/storage/src/database/devices.rs | 9 +- crates/storage/src/database/mod.rs | 13 +- crates/storage/src/database/subscriptions.rs | 125 ----- crates/storage/src/database/support.rs | 45 -- crates/storage/src/database/wallets.rs | 107 +++- crates/storage/src/lib.rs | 15 +- .../2023-07-29-000000_charts/down.sql | 1 - .../2023-07-29-000000_charts/up.sql | 8 - .../2023-09-04-220616_subscriptions/down.sql | 1 - .../2023-09-04-220616_subscriptions/up.sql | 13 - .../2025-09-15-170321_support/down.sql | 1 - .../2025-09-15-170321_support/up.sql | 13 - .../2025-12-09-120000_add_wallets/down.sql | 1 + .../2025-12-09-120000_add_wallets/up.sql | 1 + .../up.sql | 1 + crates/storage/src/models/mod.rs | 6 +- crates/storage/src/models/subscription.rs | 49 -- .../models/subscription_address_exclude.rs | 10 + crates/storage/src/models/support.rs | 21 - .../src/repositories/charts_repository.rs | 19 +- .../src/repositories/devices_repository.rs | 4 +- crates/storage/src/repositories/mod.rs | 2 - .../src/repositories/rewards_repository.rs | 6 +- .../repositories/subscriptions_repository.rs | 83 --- .../src/repositories/support_repository.rs | 37 -- .../src/repositories/wallets_repository.rs | 33 +- crates/storage/src/schema.rs | 29 -- crates/streamer/src/connection.rs | 18 +- crates/streamer/src/consumer.rs | 11 +- crates/streamer/src/lib.rs | 44 ++ crates/streamer/src/payload.rs | 9 +- crates/streamer/src/queue.rs | 3 - crates/streamer/src/steam_producer_queue.rs | 12 +- crates/streamer/src/stream_producer.rs | 48 +- crates/streamer/src/stream_reader.rs | 136 +++-- crates/support/src/client.rs | 19 +- crates/support/src/model.rs | 16 +- crates/support/tests/model_tests.rs | 18 +- .../chatwoot_conversation_updated.json | 2 +- .../testdata/chatwoot_message_created.json | 2 +- crates/tracing/src/lib.rs | 7 +- crates/yielder/src/lib.rs | 3 +- crates/yielder/src/models.rs | 15 +- crates/yielder/src/provider.rs | 16 +- crates/yielder/src/yo/provider.rs | 40 +- gemstone/src/gem_yielder/mod.rs | 95 ++-- gemstone/src/gem_yielder/remote_types.rs | 35 +- gemstone/src/models/perpetual.rs | 13 +- gemstone/src/models/stake.rs | 10 +- gemstone/src/models/transaction.rs | 59 ++- 214 files changed, 3275 insertions(+), 3200 deletions(-) delete mode 100644 apps/api/src/subscriptions/client.rs delete mode 100644 apps/api/src/subscriptions/mod.rs delete mode 100644 apps/api/src/support/client.rs delete mode 100644 apps/api/src/support/mod.rs delete mode 100644 apps/daemon/src/consumers/assets_addresses_consumer.rs delete mode 100644 apps/daemon/src/consumers/consumer_reporter.rs rename apps/daemon/src/{worker => consumers}/fiat/fiat_webhook_consumer.rs (100%) create mode 100644 apps/daemon/src/consumers/fiat/mod.rs rename apps/daemon/src/consumers/{ => indexer}/fetch_address_transactions_consumer.rs (100%) rename apps/daemon/src/consumers/{ => indexer}/fetch_assets_consumer.rs (100%) rename apps/daemon/src/consumers/{ => indexer}/fetch_blocks_consumer.rs (100%) rename apps/daemon/src/consumers/{ => indexer}/fetch_coin_addresses_consumer.rs (100%) rename apps/daemon/src/consumers/{ => indexer}/fetch_nft_assets_addresses_consumer.rs (95%) rename apps/daemon/src/consumers/{ => indexer}/fetch_token_addresses_consumer.rs (100%) create mode 100644 apps/daemon/src/consumers/indexer/mod.rs delete mode 100644 apps/daemon/src/consumers/nft/collections_updater.rs delete mode 100644 apps/daemon/src/consumers/nft/mod.rs delete mode 100644 apps/daemon/src/consumers/nft/nft_collection_asset_consumer.rs delete mode 100644 apps/daemon/src/consumers/nft/nft_collection_consumer.rs rename apps/daemon/src/consumers/{ => prices}/fetch_prices_consumer.rs (93%) create mode 100644 apps/daemon/src/consumers/prices/mod.rs create mode 100644 apps/daemon/src/consumers/rewards/mod.rs rename apps/daemon/src/consumers/{ => rewards}/rewards_consumer.rs (100%) rename apps/daemon/src/consumers/{ => rewards}/rewards_redemption_consumer.rs (100%) create mode 100644 apps/daemon/src/consumers/runner.rs create mode 100644 apps/daemon/src/consumers/store/mod.rs rename apps/daemon/src/consumers/{ => store}/store_charts_consumer.rs (100%) rename apps/daemon/src/consumers/{ => store}/store_prices_consumer.rs (100%) rename apps/daemon/src/consumers/{ => store}/store_transactions_consumer.rs (87%) rename apps/daemon/src/consumers/{ => store}/store_transactions_consumer_config.rs (100%) create mode 100644 apps/daemon/src/health.rs create mode 100644 apps/daemon/src/parser/plan.rs create mode 100644 apps/daemon/src/reporters/consumer.rs create mode 100644 apps/daemon/src/reporters/mod.rs create mode 100644 apps/daemon/src/reporters/parser.rs rename apps/daemon/src/worker/{scan => assets}/validator_scanner.rs (100%) delete mode 100644 apps/daemon/src/worker/job_history.rs delete mode 100644 apps/daemon/src/worker/job_reporter.rs create mode 100644 apps/daemon/src/worker/job_schedule.rs rename apps/daemon/src/worker/{pricer => prices}/charts_updater.rs (81%) rename apps/daemon/src/worker/{pricer => prices}/markets_updater.rs (100%) rename apps/daemon/src/worker/{pricer => prices}/mod.rs (68%) rename apps/daemon/src/worker/{pricer => prices}/observed_prices_updater.rs (100%) rename apps/daemon/src/worker/{pricer => prices}/price_updater.rs (95%) rename apps/daemon/src/worker/{prices_dex => prices}/prices_dex_updater.rs (100%) delete mode 100644 apps/daemon/src/worker/prices_dex/mod.rs delete mode 100644 apps/daemon/src/worker/scan/mod.rs rename apps/daemon/src/worker/{device => system}/device_updater.rs (100%) rename apps/daemon/src/worker/{device => system}/mod.rs (63%) rename apps/daemon/src/worker/{version => system}/model.rs (100%) rename apps/daemon/src/worker/{device => system}/observers/inactive_devices_observer.rs (87%) rename apps/daemon/src/worker/{device => system}/observers/mod.rs (100%) rename apps/daemon/src/worker/{transaction => system}/transaction_updater.rs (88%) create mode 100644 apps/daemon/src/worker/system/version_updater.rs delete mode 100644 apps/daemon/src/worker/transaction/mod.rs delete mode 100644 apps/daemon/src/worker/version/mod.rs delete mode 100644 apps/daemon/src/worker/version/version_updater.rs create mode 100644 crates/primitives/src/chain_nft.rs delete mode 100644 crates/primitives/src/consumer_status.rs delete mode 100644 crates/primitives/src/earn_action.rs delete mode 100644 crates/primitives/src/earn_position.rs create mode 100644 crates/primitives/src/earn_type.rs rename crates/primitives/src/{earn_provider.rs => growth_provider.rs} (52%) delete mode 100644 crates/primitives/src/job_status.rs create mode 100644 crates/primitives/src/metrics.rs delete mode 100644 crates/primitives/src/parser_status.rs delete mode 100644 crates/primitives/src/support.rs delete mode 100644 crates/storage/src/database/subscriptions.rs delete mode 100644 crates/storage/src/database/support.rs delete mode 100644 crates/storage/src/migrations/2025-09-15-170321_support/down.sql delete mode 100644 crates/storage/src/migrations/2025-09-15-170321_support/up.sql create mode 100644 crates/storage/src/migrations/2026-02-07-120000_drop_subscriptions/up.sql delete mode 100644 crates/storage/src/models/subscription.rs create mode 100644 crates/storage/src/models/subscription_address_exclude.rs delete mode 100644 crates/storage/src/models/support.rs delete mode 100644 crates/storage/src/repositories/subscriptions_repository.rs delete mode 100644 crates/storage/src/repositories/support_repository.rs diff --git a/Cargo.lock b/Cargo.lock index 007eaa9bc..51febffd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1958f0294ecc05ebe7b3c9a8662a3e221c2523b7f2bcd94c7a651efbd510bf" +checksum = "86debde32d8dbb0ab29e7cc75ae1a98688ac7a4c9da54b3a9b14593b9b3c46d3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f752e99497ddc39e22d547d7dfe516af10c979405a034ed90e69b914b7dddeae" +checksum = "8d6cb2e7efd385b333f5a77b71baaa2605f7e22f1d583f2879543b54cbce777c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813a67f87e56b38554d18b182616ee5006e8e2bf9df96a0df8bf29dff1d52e3f" +checksum = "be47bf1b91674a5f394b9ed3c691d764fb58ba43937f1371550ff4bc8e59c295" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -345,18 +345,18 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e43e421cb6e174846debd5fef802a59d68d409362c220b3366c850eb52e57" +checksum = "a1046d284c14c1790aed78fd6236d06a43ea6f9e419e982f4bde304e518853c5" dependencies = [ "alloy-primitives", ] [[package]] name = "alloy-json-abi" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e3cf01219c966f95a460c95f1d4c30e12f6c18150c21a30b768af2a2a29142" +checksum = "8708475665cc00e081c085886e68eada2f64cfa08fc668213a9231655093d4de" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dd146b3de349a6ffaa4e4e319ab3a90371fb159fb0bddeb1c7bbe8b1792eff" +checksum = "5a24c81a56d684f525cd1c012619815ad3a1dd13b0238f069356795d84647d3c" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c12278ffbb8872dfba3b2f17d8ea5e8503c2df5155d9bc5ee342794bde505c3" +checksum = "786c5b3ad530eaf43cda450f973fe7fb1c127b4c8990adf66709dafca25e3f6f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -407,9 +407,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833037c04917bc2031541a60e8249e4ab5500e24c637c1c62e95e963a655d66f" +checksum = "c1ed40adf21ae4be786ef5eb62db9c692f6a30f86d34452ca3f849d6390ce319" dependencies = [ "alloy-consensus", "alloy-eips", @@ -420,9 +420,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" +checksum = "3b88cf92ed20685979ed1d8472422f0c6c2d010cec77caf63aaa7669cc1a7bc2" dependencies = [ "alloy-rlp", "bytes", @@ -443,14 +443,13 @@ dependencies = [ "rustc-hash", "serde", "sha3", - "tiny-keccak", ] [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -459,9 +458,9 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", @@ -470,9 +469,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1cf5a093e437dfd62df48e480f24e1a3807632358aad6816d7a52875f1c04aa" +checksum = "d0e98aabb013a71a4b67b52825f7b503e5bb6057fb3b7b2290d514b0b0574b57" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -481,9 +480,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e97603095020543a019ab133e0e3dc38cd0819f19f19bdd70c642404a54751" +checksum = "5899af8417dcf89f40f88fa3bdb2f3f172605d8e167234311ee34811bbfdb0bf" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -502,9 +501,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946a0d413dbb5cd9adba0de5f8a1a34d5b77deda9b69c1d7feed8fc875a1aa26" +checksum = "feb73325ee881e42972a5a7bc85250f6af89f92c6ad1222285f74384a203abeb" dependencies = [ "alloy-primitives", "serde", @@ -513,9 +512,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7481dc8316768f042495eaf305d450c32defbc9bce09d8bf28afcd956895bb" +checksum = "1bea4c8f30eddb11d7ab56e83e49c814655daa78ca708df26c300c10d0189cbc" dependencies = [ "alloy-primitives", "async-trait", @@ -528,9 +527,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1259dac1f534a4c66c1d65237c89915d0010a2a91d6c3b0bada24dc5ee0fb917" +checksum = "c28bd71507db58477151a6fe6988fa62a4b778df0f166c3e3e1ef11d059fe5fa" dependencies = [ "alloy-consensus", "alloy-network", @@ -544,9 +543,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09eb18ce0df92b4277291bbaa0ed70545d78b02948df756bbd3d6214bf39a218" +checksum = "f5fa1ca7e617c634d2bd9fa71f9ec8e47c07106e248b9fcbd3eaddc13cabd625" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -558,9 +557,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d9fa2daf21f59aa546d549943f10b5cce1ae59986774019fbedae834ffe01b" +checksum = "27c00c0c3a75150a9dc7c8c679ca21853a137888b4e1c5569f92d7e2b15b5102" dependencies = [ "alloy-sol-macro-input", "const-hex", @@ -569,16 +568,16 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", + "sha3", "syn 2.0.114", "syn-solidity", - "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9396007fe69c26ee118a19f4dee1f5d1d6be186ea75b3881adf16d87f8444686" +checksum = "297db260eb4d67c105f68d6ba11b8874eec681caec5505eab8fbebee97f790bc" dependencies = [ "const-hex", "dunce", @@ -592,9 +591,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af67a0b0dcebe14244fc92002cd8d96ecbf65db4639d479f5fcd5805755a4c27" +checksum = "94b91b13181d3bcd23680fd29d7bc861d1f33fbe90fdd0af67162434aeba902d" dependencies = [ "serde", "winnow", @@ -602,9 +601,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aeea64f09a7483bdcd4193634c7e5cf9fd7775ee767585270cd8ce2d69dc95" +checksum = "fc442cc2a75207b708d481314098a0f8b6f7b58e3148dd8d8cc7407b0d6f9385" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -630,9 +629,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ceac797eb8a56bdf5ab1fab353072c17d472eab87645ca847afe720db3246d" +checksum = "6a91d6b4c2f6574fdbcb1611e460455c326667cf5b805c6bd1640dad8e8ee4d2" dependencies = [ "darling", "proc-macro2", @@ -2209,6 +2208,7 @@ dependencies = [ "prices_dex", "primitives", "reqwest 0.13.1", + "rocket", "search_index", "serde", "serde_json", @@ -3564,6 +3564,7 @@ dependencies = [ "hex", "num-bigint", "num-traits", + "number_formatter", "primitives", "reqwest 0.13.1", "serde", @@ -4533,9 +4534,9 @@ dependencies = [ [[package]] name = "keccak-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -4946,9 +4947,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -7205,9 +7206,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" dependencies = [ "cc", "cfg-if", @@ -7605,9 +7606,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f92d01b5de07eaf324f7fca61cc6bd3d82bbc1de5b6c963e6fe79e86f36580d" +checksum = "2379beea9476b89d0237078be761cf8e012d92d5ae4ae0c9a329f974838870fc" dependencies = [ "paste", "proc-macro2", @@ -7769,9 +7770,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -7784,15 +7785,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 5645af139..b5b332728 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,12 +115,12 @@ k256 = { version = "0.13.4", features = ["ecdsa", "sha256"] } uniffi = { version = "0.31.0" } regex = { version = "1.12.2" } -alloy-primitives = { version = "1.5.2", features = ["k256"] } -alloy-sol-types = { version = "1.5.2", features = ["eip712-serde"] } +alloy-primitives = { version = "1.5.4", features = ["k256"] } +alloy-sol-types = { version = "1.5.4", features = ["eip712-serde"] } alloy-dyn-abi = { version = "1.5.2", features = ["eip712"] } -alloy-json-abi = { version = "1.5.2" } -alloy-signer = { version = "1.5.2" } -alloy-signer-local = { version = "1.5.2" } -alloy-network = { version = "1.5.2" } -alloy-consensus = { version = "1.5.2" } -alloy-rlp = { version = "0.3.12" } +alloy-json-abi = { version = "1.5.4" } +alloy-signer = { version = "1.6.1" } +alloy-signer-local = { version = "1.6.1" } +alloy-network = { version = "1.6.1" } +alloy-consensus = { version = "1.6.1" } +alloy-rlp = { version = "0.3.13" } diff --git a/Settings.yaml b/Settings.yaml index 4de904d92..eb596470e 100644 --- a/Settings.yaml +++ b/Settings.yaml @@ -9,8 +9,9 @@ meilisearch: rabbitmq: url: "amqp://username:password@localhost:5672" prefetch: 50 - retry_delay: 1s - retry_max_delay: 30s + retry: + delay: 1s + timeout: 30s coingecko: key: secret: "" diff --git a/apps/api/src/assets/cilent.rs b/apps/api/src/assets/cilent.rs index 789bdabe9..5b1f57078 100644 --- a/apps/api/src/assets/cilent.rs +++ b/apps/api/src/assets/cilent.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, Utc}; use pricer::PriceClient; use primitives::{Asset, AssetBasic, AssetFull, AssetId, ChainAddress, NFTCollection, PerpetualSearchData}; use search_index::{ASSETS_INDEX_NAME, AssetDocument, NFTDocument, NFTS_INDEX_NAME, PERPETUALS_INDEX_NAME, PerpetualDocument, SearchIndexClient}; -use storage::{AssetsAddressesRepository, AssetsRepository, Database, SubscriptionsRepository, WalletsRepository}; +use storage::{AssetsAddressesRepository, AssetsRepository, Database, WalletsRepository}; #[derive(Clone)] pub struct AssetsClient { @@ -37,14 +37,6 @@ impl AssetsClient { Ok(self.database.assets()?.get_asset_full(asset_id)?) } - pub fn get_assets_by_device_id(&self, device_id: &str, wallet_index: i32, from_timestamp: Option) -> Result, Box> { - let subscriptions = self.database.subscriptions()?.get_subscriptions_by_device_id(device_id, Some(wallet_index))?; - let chain_addresses = subscriptions.into_iter().map(|x| ChainAddress::new(x.chain, x.address)).collect(); - let from_datetime = from_timestamp.and_then(|ts| DateTime::::from_timestamp(ts as i64, 0).map(|dt| dt.naive_utc())); - - Ok(self.database.assets_addresses()?.get_assets_by_addresses(chain_addresses, from_datetime, true)?) - } - pub fn get_assets_by_wallet_id(&self, device_id: i32, wallet_id: i32, from_timestamp: Option) -> Result, Box> { let subscriptions = self.database.wallets()?.get_subscriptions_by_wallet_id(device_id, wallet_id)?; let chain_addresses: Vec = subscriptions.into_iter().map(|(sub, addr)| ChainAddress::new(sub.chain.0, addr.address)).collect(); diff --git a/apps/api/src/assets/mod.rs b/apps/api/src/assets/mod.rs index 99e72028d..4d12218c8 100644 --- a/apps/api/src/assets/mod.rs +++ b/apps/api/src/assets/mod.rs @@ -2,16 +2,25 @@ pub mod cilent; mod filter; mod model; -use crate::params::{AssetIdParam, DeviceIdParam, SearchQueryParam}; +use crate::params::{AssetIdParam, SearchQueryParam}; use crate::responders::{ApiError, ApiResponse}; pub use cilent::{AssetsClient, SearchClient}; pub use model::SearchRequest; -use primitives::{Asset, AssetBasic, AssetFull, AssetId, SearchResponse}; +use pricer::PriceClient; +use primitives::{Asset, AssetBasic, AssetFull, AssetId, DEFAULT_FIAT_CURRENCY, SearchResponse}; use rocket::{State, get, post, serde::json::Json, tokio::sync::Mutex}; -#[get("/assets/")] -pub async fn get_asset(asset_id: AssetIdParam, client: &State>) -> Result, ApiError> { - Ok(client.lock().await.get_asset_full(&asset_id.0)?.into()) +#[get("/assets/?")] +pub async fn get_asset( + asset_id: AssetIdParam, + currency: Option<&str>, + client: &State>, + price_client: &State>, +) -> Result, ApiError> { + let asset = client.lock().await.get_asset_full(&asset_id.0)?; + let currency = currency.unwrap_or(DEFAULT_FIAT_CURRENCY); + let rate = price_client.lock().await.get_fiat_rate(currency)?.rate; + Ok(asset.with_rate(rate).into()) } #[post("/assets", format = "json", data = "")] @@ -50,17 +59,6 @@ pub async fn get_assets_search( Ok(client.lock().await.get_assets_search(&request).await?.into()) } -// TODO: Remove once all clients migrate to /v1/devices//wallets//assets -#[get("/assets/device/?&")] -pub async fn get_assets_by_device_id( - device_id: DeviceIdParam, - wallet_index: i32, - from_timestamp: Option, - client: &State>, -) -> Result>, ApiError> { - Ok(client.lock().await.get_assets_by_device_id(&device_id.0, wallet_index, from_timestamp)?.into()) -} - #[get("/search?&&&&")] pub async fn get_search( query: SearchQueryParam, diff --git a/apps/api/src/auth/guard.rs b/apps/api/src/auth/guard.rs index 58a057a1c..9fdc542b5 100644 --- a/apps/api/src/auth/guard.rs +++ b/apps/api/src/auth/guard.rs @@ -127,12 +127,6 @@ impl<'r, T: DeserializeOwned + Send> FromData<'r> for WalletSigned { Err(outcome) => return outcome, }; - if let Some(url_device_id) = req.routed_segment(1).map(|s: &str| s.to_string()) - && url_device_id != verified.device_id - { - return error_outcome(req, Status::Unauthorized, "Device ID mismatch"); - } - Success(WalletSigned { address: verified.address, data: verified.data, diff --git a/apps/api/src/catchers.rs b/apps/api/src/catchers.rs index d605b081f..1aa6de431 100644 --- a/apps/api/src/catchers.rs +++ b/apps/api/src/catchers.rs @@ -1,4 +1,5 @@ use crate::responders::ErrorContext; +use gem_tracing::info_with_fields; use primitives::ResponseResult; use rocket::http::Status; use rocket::serde::json::Json; @@ -12,5 +13,8 @@ pub fn default_catcher(status: Status, req: &Request) -> (Status, Json, - client: &State>, -) -> Result, ApiError> { - Ok(client.lock().await.add_support_device(&request.support_device_id, &device.device_row.device_id)?.into()) -} - #[get("/devices/subscriptions")] pub async fn get_device_subscriptions_v2(device: AuthenticatedDevice, client: &State>) -> Result>, ApiError> { Ok(client.lock().await.get_subscriptions(&device.device_row.device_id).await?.into()) diff --git a/apps/api/src/fiat/client.rs b/apps/api/src/fiat/client.rs index 75c7e9d09..28a6255c5 100644 --- a/apps/api/src/fiat/client.rs +++ b/apps/api/src/fiat/client.rs @@ -47,7 +47,13 @@ impl FiatQuotesClient { self.fiat_client.get_quotes(request).await } - pub async fn get_quote_url_legacy(&self, quote_id: &str, wallet_address: &str, ip_address: &str, device_id: &str) -> Result<(FiatQuoteUrl, FiatQuote), Box> { + pub async fn get_quote_url_legacy( + &self, + quote_id: &str, + wallet_address: &str, + ip_address: &str, + device_id: &str, + ) -> Result<(FiatQuoteUrl, FiatQuote), Box> { self.fiat_client.get_quote_url_legacy(quote_id, wallet_address, ip_address, device_id).await } diff --git a/apps/api/src/fiat/mod.rs b/apps/api/src/fiat/mod.rs index fc9c4e48a..dd8587ed4 100644 --- a/apps/api/src/fiat/mod.rs +++ b/apps/api/src/fiat/mod.rs @@ -74,7 +74,11 @@ pub async fn get_fiat_quotes_by_type( pub async fn get_fiat_quote_url(request: Json, ip: std::net::IpAddr, client: &State>) -> Result, ApiError> { let ip_address = if cfg!(debug_assertions) { DEBUG_FIAT_IP } else { &ip.to_string() }; - let (url, quote) = client.lock().await.get_quote_url_legacy(&request.quote_id, &request.wallet_address, ip_address, &request.device_id).await?; + let (url, quote) = client + .lock() + .await + .get_quote_url_legacy(&request.quote_id, &request.wallet_address, ip_address, &request.device_id) + .await?; metrics_fiat_quote_url("e); Ok(url.into()) } diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index db77cb2ab..1ce575f36 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -18,8 +18,6 @@ mod referral; mod responders; mod scan; mod status; -mod subscriptions; -mod support; mod swap; mod transactions; mod wallets; @@ -54,8 +52,6 @@ use settings::Settings; use settings_chain::{ChainProviders, ProviderFactory}; use storage::Database; use streamer::{StreamProducer, StreamProducerConfig}; -use subscriptions::SubscriptionsClient; -use support::SupportClient; use swap::SwapClient; use transactions::TransactionsClient; use wallets::WalletsClient; @@ -79,12 +75,12 @@ async fn rocket_api(settings: Settings) -> Rocket { let chain_client = chain::ChainClient::new(ChainProviders::new(ProviderFactory::new_providers(&settings))); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); let pusher_client = PusherClient::new(settings.pusher.url, settings.pusher.ios.topic); let devices_client = DevicesClient::new(database.clone(), pusher_client.clone()); let transactions_client = TransactionsClient::new(database.clone()); let stream_producer = StreamProducer::new(&rabbitmq_config, "api").await.unwrap(); - let subscriptions_client = SubscriptionsClient::new(database.clone(), stream_producer.clone()); let device_cacher = DeviceCacher::new(database.clone(), cacher_client.clone()); let wallets_client = WalletsClient::new(database.clone(), device_cacher, stream_producer.clone()); let metrics_cacher = CacherClient::new(&settings.metrics.redis.url).await; @@ -111,7 +107,6 @@ async fn rocket_api(settings: Settings) -> Rocket { let auth_client = Arc::new(AuthClient::new(cacher_client.clone())); let markets_client = MarketsClient::new(database.clone(), cacher_client.clone()); let webhooks_client = WebhooksClient::new(stream_producer.clone()); - let support_client = SupportClient::new(database.clone()); let ip_check_providers: Vec> = vec![ Arc::new(AbuseIPDBClient::new(settings.ip.abuseipdb.url.clone(), settings.ip.abuseipdb.key.secret.clone())), Arc::new(IpApiClient::new(settings.ip.ipapi.url.clone(), settings.ip.ipapi.key.secret.clone())), @@ -133,7 +128,6 @@ async fn rocket_api(settings: Settings) -> Rocket { .manage(Mutex::new(devices_client)) .manage(Mutex::new(assets_client)) .manage(Mutex::new(search_client)) - .manage(Mutex::new(subscriptions_client)) .manage(Mutex::new(transactions_client)) .manage(Mutex::new(metrics_client)) .manage(Mutex::new(scan_client)) @@ -143,7 +137,6 @@ async fn rocket_api(settings: Settings) -> Rocket { .manage(Mutex::new(chain_client)) .manage(Mutex::new(markets_client)) .manage(Mutex::new(webhooks_client)) - .manage(Mutex::new(support_client)) .manage(Mutex::new(fiat_ip_check_client)) .manage(Mutex::new(rewards_client)) .manage(Mutex::new(redemption_client)) @@ -178,18 +171,12 @@ async fn rocket_api(settings: Settings) -> Rocket { assets::get_assets, assets::add_asset, assets::get_assets_search, - assets::get_assets_by_device_id, assets::get_search, - subscriptions::add_subscriptions, - subscriptions::get_subscriptions, - subscriptions::delete_subscriptions, - transactions::get_transactions_by_device_id_v1, transactions::get_transaction_by_id, chain::transaction::get_latest_block_number, chain::transaction::get_block_transactions, chain::transaction::get_block_transactions_finalize, swap::get_swap_assets, - nft::get_nft_assets_old, nft::get_nft_assets_by_chain, nft::get_nft_collection, nft::get_nft_asset, @@ -211,9 +198,6 @@ async fn rocket_api(settings: Settings) -> Rocket { chain::balance::get_balances_staking, chain::transaction::get_transactions, webhooks::create_support_webhook, - support::add_device_legacy, - support::add_device, - support::get_support_device, fiat::get_ip_address, referral::get_rewards_leaderboard, referral::get_rewards_redemption_option, @@ -231,8 +215,6 @@ async fn rocket_api(settings: Settings) -> Rocket { routes![ devices::get_fiat_quotes_v2, devices::get_fiat_quote_url_v2, - transactions::get_transactions_by_device_id_v2, - nft::get_nft_assets_v2, scan::scan_transaction_v2, devices::add_device_v2, devices::get_device_v2, @@ -253,7 +235,6 @@ async fn rocket_api(settings: Settings) -> Rocket { devices::redeem_device_rewards_v2, devices::get_device_notifications_v2, devices::mark_device_notifications_read_v2, - devices::add_device_support_v2, devices::get_device_subscriptions_v2, devices::add_device_subscriptions_v2, devices::delete_device_subscriptions_v2, diff --git a/apps/api/src/metrics/client.rs b/apps/api/src/metrics/client.rs index 45b076a35..917c089f0 100644 --- a/apps/api/src/metrics/client.rs +++ b/apps/api/src/metrics/client.rs @@ -23,7 +23,7 @@ impl MetricsClient { pub async fn get(&self) -> String { super::parser::update_parser_metrics(&self.database, &self.cacher).await; - super::price::update_price_metrics(&self.database); + super::price::update_price_metrics(&self.database, &self.cacher).await; super::job::update_job_metrics(&self.cacher).await; super::consumer::update_consumer_metrics(&self.cacher).await; diff --git a/apps/api/src/metrics/consumer.rs b/apps/api/src/metrics/consumer.rs index 05952f468..fdee380eb 100644 --- a/apps/api/src/metrics/consumer.rs +++ b/apps/api/src/metrics/consumer.rs @@ -11,8 +11,9 @@ const CONSUMERS_STATUS_PREFIX: &str = "consumers:status:"; static CONSUMER_PROCESSED: OnceLock> = OnceLock::new(); static CONSUMER_ERRORS: OnceLock> = OnceLock::new(); static CONSUMER_LAST_SUCCESS_AT: OnceLock> = OnceLock::new(); -static CONSUMER_AVG_DURATION_MS: OnceLock> = OnceLock::new(); +static CONSUMER_AVG_DURATION: OnceLock> = OnceLock::new(); static CONSUMER_ERROR_DETAIL: OnceLock> = OnceLock::new(); +static CONSUMER_LAST_ERROR_AT: OnceLock> = OnceLock::new(); #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct ConsumerLabels { @@ -31,18 +32,21 @@ pub fn init_consumer_metrics(registry: &mut Registry) { let last_success = Family::::default(); let avg_duration = Family::::default(); let error_detail = Family::::default(); + let error_at = Family::::default(); registry.register("consumer_processed", "Messages processed", processed.clone()); registry.register("consumer_errors", "Errors encountered", errors.clone()); registry.register("consumer_last_success_at", "Last successful processing (unix timestamp)", last_success.clone()); - registry.register("consumer_avg_duration_ms", "Average processing duration in milliseconds", avg_duration.clone()); + registry.register("consumer_avg_duration_milliseconds", "Average processing duration in milliseconds", avg_duration.clone()); registry.register("consumer_error_detail", "Error occurrence count per consumer and error message", error_detail.clone()); + registry.register("consumer_last_error_at", "Last error timestamp (unix)", error_at.clone()); CONSUMER_PROCESSED.set(processed).ok(); CONSUMER_ERRORS.set(errors).ok(); CONSUMER_LAST_SUCCESS_AT.set(last_success).ok(); - CONSUMER_AVG_DURATION_MS.set(avg_duration).ok(); + CONSUMER_AVG_DURATION.set(avg_duration).ok(); CONSUMER_ERROR_DETAIL.set(error_detail).ok(); + CONSUMER_LAST_ERROR_AT.set(error_at).ok(); } pub async fn update_consumer_metrics(cacher: &CacherClient) { @@ -68,17 +72,20 @@ pub async fn update_consumer_metrics(cacher: &CacherClient) { if let (Some(family), Some(ts)) = (CONSUMER_LAST_SUCCESS_AT.get(), status.last_success) { family.get_or_create(&labels).set(ts as i64); } - if let Some(family) = CONSUMER_AVG_DURATION_MS.get() { + if let Some(family) = CONSUMER_AVG_DURATION.get() { family.get_or_create(&labels).set(status.avg_duration as i64); } - if let Some(family) = CONSUMER_ERROR_DETAIL.get() { - for err in &status.errors { - let error_labels = ConsumerErrorLabels { - consumer: name.to_string(), - error: err.message.clone(), - }; + for err in &status.errors { + let error_labels = ConsumerErrorLabels { + consumer: name.to_string(), + error: err.message.clone(), + }; + if let Some(family) = CONSUMER_ERROR_DETAIL.get() { family.get_or_create(&error_labels).set(err.count as i64); } + if let Some(family) = CONSUMER_LAST_ERROR_AT.get() { + family.get_or_create(&error_labels).set(err.timestamp as i64); + } } } } diff --git a/apps/api/src/metrics/fiat.rs b/apps/api/src/metrics/fiat.rs index 6146d00e0..4f787eac4 100644 --- a/apps/api/src/metrics/fiat.rs +++ b/apps/api/src/metrics/fiat.rs @@ -3,9 +3,11 @@ use primitives::{FiatQuote, FiatQuotes}; use prometheus_client::encoding::EncodeLabelSet; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; +use prometheus_client::metrics::gauge::Gauge; use prometheus_client::metrics::histogram::Histogram; use prometheus_client::registry::Registry; use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; const FIAT_AMOUNT_BUCKETS: [f64; 6] = [50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0]; @@ -14,6 +16,9 @@ static FIAT_QUOTE_SUCCESS: OnceLock> = OnceLock static FIAT_QUOTE_ERROR: OnceLock> = OnceLock::new(); static FIAT_QUOTE_AMOUNT: OnceLock> = OnceLock::new(); static FIAT_QUOTE_URL_GENERATED: OnceLock> = OnceLock::new(); +static FIAT_QUOTE_ERRORS: OnceLock> = OnceLock::new(); +static FIAT_QUOTE_ERROR_DETAIL: OnceLock> = OnceLock::new(); +static FIAT_QUOTE_LAST_ERROR_AT: OnceLock> = OnceLock::new(); #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct FiatQuoteLabels { @@ -27,6 +32,12 @@ struct FiatQuoteErrorLabels { provider: String, } +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +struct FiatQuoteErrorDetailLabels { + provider: String, + error: String, +} + pub fn init_fiat_metrics(registry: &mut Registry) { let latency = Family::::new_with_constructor(histogram::latency); let success = Family::::default(); @@ -34,17 +45,27 @@ pub fn init_fiat_metrics(registry: &mut Registry) { let amount = Family::::new_with_constructor(|| Histogram::new(FIAT_AMOUNT_BUCKETS)); let url_generated = Family::::default(); + let errors = Family::::default(); + let error_detail = Family::::default(); + let last_error_at = Family::::default(); + registry.register("fiat_quote_latency", "Fiat provider quote latency in seconds", latency.clone()); registry.register("fiat_quote_success", "Successful fiat quotes", success.clone()); registry.register("fiat_quote_error", "Failed fiat quotes", error.clone()); registry.register("fiat_quote_amount", "Fiat quote amount distribution", amount.clone()); registry.register("fiat_quote_url_generated", "Fiat quote URLs generated", url_generated.clone()); + registry.register("fiat_quote_errors", "Total fiat quote errors", errors.clone()); + registry.register("fiat_quote_error_detail", "Fiat quote error details by provider and message", error_detail.clone()); + registry.register("fiat_quote_last_error_at", "Last fiat quote error timestamp (unix)", last_error_at.clone()); FIAT_QUOTE_LATENCY.set(latency).ok(); FIAT_QUOTE_SUCCESS.set(success).ok(); FIAT_QUOTE_ERROR.set(error).ok(); FIAT_QUOTE_AMOUNT.set(amount).ok(); FIAT_QUOTE_URL_GENERATED.set(url_generated).ok(); + FIAT_QUOTE_ERRORS.set(errors).ok(); + FIAT_QUOTE_ERROR_DETAIL.set(error_detail).ok(); + FIAT_QUOTE_LAST_ERROR_AT.set(last_error_at).ok(); } pub fn metrics_fiat_quotes(quotes: &FiatQuotes) { @@ -73,6 +94,26 @@ pub fn metrics_fiat_quotes(quotes: &FiatQuotes) { if let Some(error_metric) = FIAT_QUOTE_ERROR.get() { error_metric.get_or_create(&labels).inc(); } + if let Some(errors) = FIAT_QUOTE_ERRORS.get() { + errors.get_or_create(&labels).inc(); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + + if let Some(last_error_at) = FIAT_QUOTE_LAST_ERROR_AT.get() { + last_error_at.get_or_create(&labels).set(now); + } + + let detail_labels = FiatQuoteErrorDetailLabels { + provider: provider.clone(), + error: error.error.clone(), + }; + if let Some(error_detail) = FIAT_QUOTE_ERROR_DETAIL.get() { + error_detail.get_or_create(&detail_labels).set(now); + } } } } diff --git a/apps/api/src/metrics/job.rs b/apps/api/src/metrics/job.rs index b57bf227a..96a2a415c 100644 --- a/apps/api/src/metrics/job.rs +++ b/apps/api/src/metrics/job.rs @@ -9,10 +9,11 @@ use std::sync::OnceLock; const JOBS_STATUS_PREFIX: &str = "jobs:status:"; static JOB_LAST_SUCCESS_AT: OnceLock> = OnceLock::new(); -static JOB_INTERVAL_SECONDS: OnceLock> = OnceLock::new(); +static JOB_INTERVAL: OnceLock> = OnceLock::new(); static JOB_LAST_ERROR_AT: OnceLock> = OnceLock::new(); -static JOB_DURATION_MS: OnceLock> = OnceLock::new(); -static JOB_LAST_ERROR: OnceLock> = OnceLock::new(); +static JOB_DURATION: OnceLock> = OnceLock::new(); +static JOB_ERROR_DETAIL: OnceLock> = OnceLock::new(); +static JOB_ERRORS: OnceLock> = OnceLock::new(); #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct JobErrorLabels { @@ -33,18 +34,21 @@ pub fn init_job_metrics(registry: &mut Registry) { let last_error = Family::::default(); let duration = Family::::default(); let last_error_detail = Family::::default(); + let error_count = Family::::default(); registry.register("job_last_success_at", "Last successful job run (unix timestamp)", last_success.clone()); registry.register("job_interval_seconds", "Job interval in seconds", interval.clone()); registry.register("job_last_error_at", "Last job error (unix timestamp)", last_error.clone()); - registry.register("job_duration_ms", "Last job duration in milliseconds", duration.clone()); - registry.register("job_last_error", "Last job error message", last_error_detail.clone()); + registry.register("job_duration_milliseconds", "Last job duration in milliseconds", duration.clone()); + registry.register("job_error_detail", "Job error details by service and message", last_error_detail.clone()); + registry.register("job_errors", "Total error count", error_count.clone()); JOB_LAST_SUCCESS_AT.set(last_success).ok(); - JOB_INTERVAL_SECONDS.set(interval).ok(); + JOB_INTERVAL.set(interval).ok(); JOB_LAST_ERROR_AT.set(last_error).ok(); - JOB_DURATION_MS.set(duration).ok(); - JOB_LAST_ERROR.set(last_error_detail).ok(); + JOB_DURATION.set(duration).ok(); + JOB_ERROR_DETAIL.set(last_error_detail).ok(); + JOB_ERRORS.set(error_count).ok(); } pub async fn update_job_metrics(cacher: &CacherClient) { @@ -64,19 +68,22 @@ pub async fn update_job_metrics(cacher: &CacherClient) { job_name: job_name.to_string(), }; - if let Some(family) = JOB_INTERVAL_SECONDS.get() { + if let Some(family) = JOB_INTERVAL.get() { family.get_or_create(&labels).set(status.interval as i64); } - if let Some(family) = JOB_DURATION_MS.get() { + if let Some(family) = JOB_DURATION.get() { family.get_or_create(&labels).set(status.duration as i64); } + if let Some(family) = JOB_ERRORS.get() { + family.get_or_create(&labels).set(status.error_count as i64); + } if let (Some(family), Some(ts)) = (JOB_LAST_SUCCESS_AT.get(), status.last_success) { family.get_or_create(&labels).set(ts as i64); } if let (Some(family), Some(ts)) = (JOB_LAST_ERROR_AT.get(), status.last_error_at) { family.get_or_create(&labels).set(ts as i64); } - if let (Some(family), Some(msg), Some(ts)) = (JOB_LAST_ERROR.get(), &status.last_error, status.last_error_at) { + if let (Some(family), Some(msg), Some(ts)) = (JOB_ERROR_DETAIL.get(), &status.last_error, status.last_error_at) { let error_labels = JobErrorLabels { service: service.to_string(), job_name: job_name.to_string(), diff --git a/apps/api/src/metrics/parser.rs b/apps/api/src/metrics/parser.rs index a4664f41b..87d3d7bb0 100644 --- a/apps/api/src/metrics/parser.rs +++ b/apps/api/src/metrics/parser.rs @@ -13,6 +13,7 @@ static PARSER_IS_ENABLED: OnceLock> = OnceLock: static PARSER_UPDATED_AT: OnceLock> = OnceLock::new(); static PARSER_ERRORS: OnceLock> = OnceLock::new(); static PARSER_ERROR_DETAIL: OnceLock> = OnceLock::new(); +static PARSER_LAST_ERROR_AT: OnceLock> = OnceLock::new(); #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct ParserStateLabels { @@ -32,6 +33,7 @@ pub fn init_parser_metrics(registry: &mut Registry) { let updated_at = Family::::default(); let errors = Family::::default(); let error_detail = Family::::default(); + let last_error_at = Family::::default(); registry.register("parser_state_latest_block", "Parser latest block", latest_block.clone()); registry.register("parser_state_current_block", "Parser current block", current_block.clone()); @@ -39,6 +41,7 @@ pub fn init_parser_metrics(registry: &mut Registry) { registry.register("parser_state_updated_at", "Parser updated at", updated_at.clone()); registry.register("parser_errors", "Parser errors encountered", errors.clone()); registry.register("parser_error_detail", "Parser error details by chain and message", error_detail.clone()); + registry.register("parser_last_error_at", "Parser last error timestamp (unix)", last_error_at.clone()); PARSER_LATEST_BLOCK.set(latest_block).ok(); PARSER_CURRENT_BLOCK.set(current_block).ok(); @@ -46,6 +49,7 @@ pub fn init_parser_metrics(registry: &mut Registry) { PARSER_UPDATED_AT.set(updated_at).ok(); PARSER_ERRORS.set(errors).ok(); PARSER_ERROR_DETAIL.set(error_detail).ok(); + PARSER_LAST_ERROR_AT.set(last_error_at).ok(); } pub async fn update_parser_metrics(database: &Database, cacher: &CacherClient) { @@ -79,13 +83,15 @@ pub async fn update_parser_metrics(database: &Database, cacher: &CacherClient) { } if let Some(error_detail) = PARSER_ERROR_DETAIL.get() { - for error in status.errors { - error_detail - .get_or_create(&ParserErrorLabels { - chain: chain.clone(), - error: error.message, - }) - .set(error.count as i64); + for error in &status.errors { + let labels = ParserErrorLabels { + chain: chain.clone(), + error: error.message.clone(), + }; + error_detail.get_or_create(&labels).set(error.count as i64); + if let Some(last_error_at) = PARSER_LAST_ERROR_AT.get() { + last_error_at.get_or_create(&labels).set(error.timestamp as i64); + } } } } else if let Some(errors) = PARSER_ERRORS.get() { diff --git a/apps/api/src/metrics/price.rs b/apps/api/src/metrics/price.rs index 84dd0e0f3..0637b53e2 100644 --- a/apps/api/src/metrics/price.rs +++ b/apps/api/src/metrics/price.rs @@ -1,3 +1,4 @@ +use cacher::{CacheKey, CacherClient}; use prometheus_client::encoding::EncodeLabelSet; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::gauge::Gauge; @@ -8,6 +9,9 @@ use storage::Database; static PRICE_UPDATED_AT: OnceLock> = OnceLock::new(); static PRICE_VALUE: OnceLock>> = OnceLock::new(); +static PRICE_ASSETS_TOTAL: OnceLock = OnceLock::new(); +static PRICE_OBSERVED_ASSETS_TOTAL: OnceLock = OnceLock::new(); +static PRICE_OBSERVED_ASSET_SCORE: OnceLock>> = OnceLock::new(); #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct PriceLabels { @@ -17,31 +21,61 @@ struct PriceLabels { pub fn init_price_metrics(registry: &mut Registry) { let updated_at = Family::::default(); let value = Family::>::default(); + let assets_total = Gauge::default(); + let observed_total = Gauge::default(); + let observed_score = Family::>::default(); registry.register("price_updated_at", "Price updated at", updated_at.clone()); registry.register("price_value", "Price value", value.clone()); + registry.register("price_assets_total", "Total number of tracked price assets", assets_total.clone()); + registry.register("price_observed_assets_total", "Total unique assets being observed via WebSocket", observed_total.clone()); + registry.register("price_observed_asset_score", "Observer count for top observed assets", observed_score.clone()); PRICE_UPDATED_AT.set(updated_at).ok(); PRICE_VALUE.set(value).ok(); + PRICE_ASSETS_TOTAL.set(assets_total).ok(); + PRICE_OBSERVED_ASSETS_TOTAL.set(observed_total).ok(); + PRICE_OBSERVED_ASSET_SCORE.set(observed_score).ok(); } -pub fn update_price_metrics(database: &Database) { - let prices = database - .client() - .ok() - .and_then(|mut c| c.prices().get_prices().ok()) - .unwrap_or_default() - .into_iter() - .filter(|x| x.market_cap_rank >= 1 && x.market_cap_rank <= 10); - - for price in prices { - if let Some(updated_at) = PRICE_UPDATED_AT.get() { - updated_at - .get_or_create(&PriceLabels { asset_id: price.clone().id }) - .set(price.last_updated_at.and_utc().timestamp()); +pub async fn update_price_metrics(database: &Database, cacher: &CacherClient) { + update_db_price_metrics(database); + update_observed_metrics(cacher).await; +} + +fn update_db_price_metrics(database: &Database) { + let prices = database.client().ok().and_then(|mut c| c.prices().get_prices().ok()).unwrap_or_default(); + + if let Some(gauge) = PRICE_ASSETS_TOTAL.get() { + gauge.set(prices.len() as i64); + } + + for price in prices.into_iter().filter(|x| x.market_cap_rank >= 1 && x.market_cap_rank <= 10) { + let labels = PriceLabels { asset_id: price.id.clone() }; + if let Some(family) = PRICE_UPDATED_AT.get() { + family.get_or_create(&labels).set(price.last_updated_at.and_utc().timestamp()); + } + if let Some(family) = PRICE_VALUE.get() { + family.get_or_create(&labels).set(price.price); } - if let Some(value) = PRICE_VALUE.get() { - value.get_or_create(&PriceLabels { asset_id: price.clone().id }).set(price.price); + } +} + +async fn update_observed_metrics(cacher: &CacherClient) { + let key = CacheKey::ObservedAssets; + + if let Ok(count) = cacher.sorted_set_card(&key.key()).await + && let Some(gauge) = PRICE_OBSERVED_ASSETS_TOTAL.get() + { + gauge.set(count as i64); + } + + if let Some(family) = PRICE_OBSERVED_ASSET_SCORE.get() { + family.clear(); + if let Ok(top_assets) = cacher.sorted_set_rev_range_with_scores(&key.key(), 0, 9).await { + for (asset_id, score) in top_assets { + family.get_or_create(&PriceLabels { asset_id }).set(score); + } } } } diff --git a/apps/api/src/nft/mod.rs b/apps/api/src/nft/mod.rs index 825b1d356..65279e52f 100644 --- a/apps/api/src/nft/mod.rs +++ b/apps/api/src/nft/mod.rs @@ -1,7 +1,7 @@ -use crate::params::{AddressParam, ChainParam, DeviceIdParam, NftAssetIdParam, NftCollectionIdParam}; +use crate::params::{AddressParam, ChainParam, NftAssetIdParam, NftCollectionIdParam}; use crate::responders::{ApiError, ApiResponse}; use ::nft::NFTClient; -use primitives::{NFTAsset, NFTData, ReportNft, response::ResponseResultNew}; +use primitives::{NFTAsset, NFTData, ReportNft}; use rocket::Request; use rocket::http::ContentType; use rocket::response::{self, Responder}; @@ -10,20 +10,6 @@ use rocket::{State, get, post, put, tokio::sync::Mutex}; use std::collections::HashMap; use std::io::Cursor; -// by device - -// TODO: Remove once all clients migrate to /v1/devices//wallets//nft_assets -#[get("/nft/assets/device/?")] -pub async fn get_nft_assets_old(device_id: DeviceIdParam, wallet_index: i32, client: &State>) -> Result>>, ApiError> { - Ok(ResponseResultNew::new(client.lock().await.get_nft_assets(&device_id.0, wallet_index).await?).into()) -} - -// TODO: Remove once all clients migrate to /v1/devices//wallets//nft_assets -#[get("/nft/assets/device/?")] -pub async fn get_nft_assets_v2(device_id: DeviceIdParam, wallet_index: i32, client: &State>) -> Result>, ApiError> { - Ok(client.lock().await.get_nft_assets(&device_id.0, wallet_index).await?.into()) -} - // by address. mostly for testing purposes #[get("/nft/assets/chain/?

")] diff --git a/apps/api/src/subscriptions/client.rs b/apps/api/src/subscriptions/client.rs deleted file mode 100644 index b309c7bbc..000000000 --- a/apps/api/src/subscriptions/client.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::error::Error; - -use primitives::{ChainAddress, Subscription}; -use storage::{Database, SubscriptionsRepository}; -use streamer::{ChainAddressPayload, StreamProducer, StreamProducerQueue}; - -#[derive(Clone)] -pub struct SubscriptionsClient { - database: Database, - stream_producer: StreamProducer, -} - -impl SubscriptionsClient { - pub fn new(database: Database, stream_producer: StreamProducer) -> Self { - Self { database, stream_producer } - } - - pub async fn add_subscriptions(&self, device_id: &str, subscriptions: Vec) -> Result> { - let result = self.database.subscriptions()?.add_subscriptions(subscriptions.clone(), device_id); - let payload = subscriptions - .clone() - .into_iter() - .map(|x| ChainAddressPayload::new(ChainAddress::new(x.chain, x.address))) - .collect::>(); - self.stream_producer.publish_new_addresses(payload).await?; - Ok(result?) - } - - pub async fn get_subscriptions_by_device_id(&self, device_id: &str) -> Result, Box> { - Ok(self.database.subscriptions()?.get_subscriptions_by_device_id(device_id, None)?) - } - - pub async fn delete_subscriptions(&self, device_id: &str, subscriptions: Vec) -> Result> { - Ok(self.database.subscriptions()?.delete_subscriptions(subscriptions, device_id)?) - } -} diff --git a/apps/api/src/subscriptions/mod.rs b/apps/api/src/subscriptions/mod.rs deleted file mode 100644 index 2fa480d2f..000000000 --- a/apps/api/src/subscriptions/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub mod client; -use crate::params::DeviceIdParam; -use crate::responders::{ApiError, ApiResponse}; -pub use client::SubscriptionsClient; -use primitives::Subscription; -use rocket::{State, delete, get, post, serde::json::Json, tokio::sync::Mutex}; - -#[get("/subscriptions/")] -pub async fn get_subscriptions(device_id: DeviceIdParam, client: &State>) -> Result>, ApiError> { - Ok(client.lock().await.get_subscriptions_by_device_id(&device_id.0).await?.into()) -} - -#[delete("/subscriptions/", format = "json", data = "")] -pub async fn delete_subscriptions( - subscriptions: Json>, - device_id: DeviceIdParam, - client: &State>, -) -> Result, ApiError> { - Ok(client.lock().await.delete_subscriptions(&device_id.0, subscriptions.0).await?.into()) -} - -#[post("/subscriptions/", format = "json", data = "")] -pub async fn add_subscriptions( - subscriptions: Json>, - device_id: DeviceIdParam, - client: &State>, -) -> Result, ApiError> { - Ok(client.lock().await.add_subscriptions(&device_id.0, subscriptions.0).await?.into()) -} diff --git a/apps/api/src/support/client.rs b/apps/api/src/support/client.rs deleted file mode 100644 index 40992ec7e..000000000 --- a/apps/api/src/support/client.rs +++ /dev/null @@ -1,22 +0,0 @@ -use primitives::SupportDevice; -use std::error::Error; -use storage::{Database, SupportRepository}; - -#[derive(Clone)] -pub struct SupportClient { - database: Database, -} - -impl SupportClient { - pub fn new(database: Database) -> Self { - Self { database } - } - - pub fn add_support_device(&self, support_id: &str, device_id: &str) -> Result> { - Ok(self.database.support()?.add_support_device(support_id, device_id)?) - } - - pub fn get_support_device(&self, support_id: &str) -> Result> { - Ok(self.database.support()?.get_support(support_id)?) - } -} diff --git a/apps/api/src/support/mod.rs b/apps/api/src/support/mod.rs deleted file mode 100644 index 307d71242..000000000 --- a/apps/api/src/support/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub mod client; - -use crate::devices::guard::AuthenticatedDevice; -use crate::responders::{ApiError, ApiResponse}; -pub use client::SupportClient; -use primitives::{NewSupportDevice, SupportDevice, SupportDeviceRequest}; -use rocket::{State, get, post, serde::json::Json, tokio::sync::Mutex}; - -#[post("/support/add_device", format = "json", data = "")] -pub async fn add_device_legacy(request: Json, client: &State>) -> Result, ApiError> { - let support_device = client.lock().await.add_support_device(&request.support_device_id, &request.device_id)?; - Ok(ApiResponse::from(support_device)) -} - -#[post("/devices/<_device_id>/support", format = "json", data = "")] -pub async fn add_device( - _device_id: &str, - device: AuthenticatedDevice, - request: Json, - client: &State>, -) -> Result, ApiError> { - let support_device = client.lock().await.add_support_device(&request.support_device_id, &device.device_row.device_id)?; - Ok(ApiResponse::from(support_device)) -} - -#[get("/support/")] -pub async fn get_support_device(support_device_id: &str, client: &State>) -> Result, ApiError> { - let support_device = client.lock().await.get_support_device(support_device_id)?; - Ok(ApiResponse::from(support_device)) -} diff --git a/apps/api/src/transactions/client.rs b/apps/api/src/transactions/client.rs index 5c1424197..d52bf3fb2 100644 --- a/apps/api/src/transactions/client.rs +++ b/apps/api/src/transactions/client.rs @@ -2,7 +2,7 @@ use std::error::Error; use chrono::{DateTime, Utc}; use primitives::{Transaction, TransactionId, TransactionsResponse}; -use storage::{Database, ScanAddressesRepository, SubscriptionsRepository, TransactionsRepository, WalletsRepository}; +use storage::{Database, ScanAddressesRepository, TransactionsRepository, WalletsRepository}; #[derive(Clone)] pub struct TransactionsClient { @@ -14,40 +14,6 @@ impl TransactionsClient { Self { database } } - pub fn get_transactions_by_device_id( - &self, - device_id: &str, - wallet_index: i32, - asset_id: Option, - from_timestamp: Option, - ) -> Result> { - let subscriptions = self.database.subscriptions()?.get_subscriptions_by_device_id(device_id, Some(wallet_index))?; - - let addresses = subscriptions.clone().into_iter().map(|x| x.address).collect::>(); - let chains = subscriptions.clone().into_iter().map(|x| x.chain.as_ref().to_string()).collect::>(); - let from_datetime = from_timestamp.and_then(|ts| DateTime::::from_timestamp(ts as i64, 0).map(|dt| dt.naive_utc())); - - let transactions = self - .database - .transactions()? - .get_transactions_by_device_id(device_id, addresses.clone(), chains.clone(), asset_id, from_datetime)? - .into_iter() - .map(|x| x.as_primitive(addresses.clone()).finalize(addresses.clone())) - .collect::>(); - - let scan_addresses = transactions.iter().flat_map(|x| x.addresses()).collect::>(); - - let address_names = self - .database - .scan_addresses()? - .get_scan_addresses_by_addresses(scan_addresses.clone())? - .into_iter() - .flat_map(|x| x.as_primitive()) - .collect(); - - Ok(TransactionsResponse::new(transactions, address_names)) - } - pub fn get_transactions_by_wallet_id( &self, device_id: &str, diff --git a/apps/api/src/transactions/mod.rs b/apps/api/src/transactions/mod.rs index 83248f482..a5efc63ec 100644 --- a/apps/api/src/transactions/mod.rs +++ b/apps/api/src/transactions/mod.rs @@ -1,43 +1,10 @@ pub mod client; -use crate::params::{DeviceIdParam, TransactionIdParam}; +use crate::params::TransactionIdParam; use crate::responders::{ApiError, ApiResponse}; pub use client::TransactionsClient; -use primitives::{Transaction, TransactionsResponse}; +use primitives::Transaction; use rocket::{State, get, tokio::sync::Mutex}; -// TODO: Remove once all clients migrate to /v1/devices//wallets//transactions -#[get("/transactions/device/?&&")] -pub async fn get_transactions_by_device_id_v1( - device_id: DeviceIdParam, - wallet_index: i32, - asset_id: Option<&str>, - from_timestamp: Option, - client: &State>, -) -> Result>, ApiError> { - Ok(client - .lock() - .await - .get_transactions_by_device_id(&device_id.0, wallet_index, asset_id.map(|s| s.to_string()), from_timestamp)? - .transactions - .into()) -} - -// TODO: Remove once all clients migrate to /v1/devices//wallets//transactions -#[get("/transactions/device/?&&")] -pub async fn get_transactions_by_device_id_v2( - device_id: DeviceIdParam, - wallet_index: i32, - asset_id: Option<&str>, - from_timestamp: Option, - client: &State>, -) -> Result, ApiError> { - Ok(client - .lock() - .await - .get_transactions_by_device_id(&device_id.0, wallet_index, asset_id.map(|s| s.to_string()), from_timestamp)? - .into()) -} - #[get("/transactions/")] pub async fn get_transaction_by_id(id: TransactionIdParam, client: &State>) -> Result, ApiError> { Ok(client.lock().await.get_transaction_by_id(&id.0)?.into()) diff --git a/apps/api/src/websocket_stream/client.rs b/apps/api/src/websocket_stream/client.rs index bbac4507d..b61b70a8f 100644 --- a/apps/api/src/websocket_stream/client.rs +++ b/apps/api/src/websocket_stream/client.rs @@ -126,6 +126,8 @@ impl StreamObserverClient { self.build_and_send_payload(stream, payload).await?; return Ok(redis_connection.subscribe(self.get_asset_ids()).await?); } + StreamMessage::SubscribeRealtimePrices(_) => return Ok(()), + StreamMessage::UnsubscribeRealtimePrices(_) => return Ok(()), }; if needs_clear { diff --git a/apps/daemon/Cargo.toml b/apps/daemon/Cargo.toml index db9ce45cb..3f46943ce 100644 --- a/apps/daemon/Cargo.toml +++ b/apps/daemon/Cargo.toml @@ -7,6 +7,7 @@ version = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["signal"] } +rocket = { workspace = true } reqwest = { workspace = true } futures = { workspace = true } chrono = { workspace = true } diff --git a/apps/daemon/src/consumers/assets_addresses_consumer.rs b/apps/daemon/src/consumers/assets_addresses_consumer.rs deleted file mode 100644 index a0cb7c97d..000000000 --- a/apps/daemon/src/consumers/assets_addresses_consumer.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::error::Error; - -use async_trait::async_trait; -use storage::Database; -use streamer::{AssetsAddressPayload, consumer::MessageConsumer}; - -pub struct AssetsAddressesConsumer { - pub database: Database, -} - -impl AssetsAddressesConsumer { - pub fn new(database: Database) -> Self { - Self { database } - } -} - -#[async_trait] -impl MessageConsumer for AssetsAddressesConsumer { - async fn should_process(&self, _payload: AssetsAddressPayload) -> Result> { - Ok(true) - } - - async fn process(&self, payload: AssetsAddressPayload) -> Result> { - let assets_addresses = payload.values.into_iter().map(storage::models::AssetAddressRow::from_primitive).collect::>(); - - Ok(self - .database - .client()? - .assets_addresses() - .add_assets_addresses(assets_addresses.clone().into_iter().map(|x| x.as_primitive()).collect())?) - } -} diff --git a/apps/daemon/src/consumers/consumer_reporter.rs b/apps/daemon/src/consumers/consumer_reporter.rs deleted file mode 100644 index de68b4efc..000000000 --- a/apps/daemon/src/consumers/consumer_reporter.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::future::Future; -use std::pin::Pin; - -use cacher::{CacheKey, CacherClient}; -use gem_tracing::info_with_fields; -use primitives::{ConsumerError, ConsumerStatus}; -use streamer::ConsumerStatusReporter; - -pub struct CacherConsumerReporter { - cacher: CacherClient, -} - -impl CacherConsumerReporter { - pub fn new(cacher: CacherClient) -> Self { - Self { cacher } - } -} - -impl ConsumerStatusReporter for CacherConsumerReporter { - fn report_success(&self, name: &str, duration: u64, result: &str) -> Pin + Send + '_>> { - let normalized = name.to_string(); - let result = result.to_string(); - Box::pin(async move { - let cache_key = CacheKey::ConsumerStatus(&normalized); - let key = cache_key.key(); - let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); - - status.total_processed += 1; - status.last_success = Some(timestamp); - status.last_result = Some(result); - - let prev_total = status.total_processed - 1; - status.avg_duration = (status.avg_duration * prev_total + duration) / status.total_processed; - - if let Err(e) = self.cacher.set_cached(cache_key, &status).await { - info_with_fields!("consumer status report failed", consumer = key, error = format!("{:?}", e)); - } - }) - } - - fn report_error(&self, name: &str, error: &str) -> Pin + Send + '_>> { - let normalized = name.to_string(); - let error = error.to_string(); - Box::pin(async move { - let cache_key = CacheKey::ConsumerStatus(&normalized); - let key = cache_key.key(); - let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); - - status.total_errors += 1; - - let truncated = if error.len() > 200 { error[..200].to_string() } else { error.clone() }; - - if let Some(entry) = status.errors.iter_mut().find(|e| e.message == truncated) { - entry.count += 1; - entry.timestamp = timestamp; - } else { - status.errors.push(ConsumerError { - message: truncated, - count: 1, - timestamp, - }); - } - - if let Err(e) = self.cacher.set_cached(cache_key, &status).await { - info_with_fields!("consumer status report failed", consumer = key, error = format!("{:?}", e)); - } - }) - } -} diff --git a/apps/daemon/src/worker/fiat/fiat_webhook_consumer.rs b/apps/daemon/src/consumers/fiat/fiat_webhook_consumer.rs similarity index 100% rename from apps/daemon/src/worker/fiat/fiat_webhook_consumer.rs rename to apps/daemon/src/consumers/fiat/fiat_webhook_consumer.rs diff --git a/apps/daemon/src/consumers/fiat/mod.rs b/apps/daemon/src/consumers/fiat/mod.rs new file mode 100644 index 000000000..f2fc4e390 --- /dev/null +++ b/apps/daemon/src/consumers/fiat/mod.rs @@ -0,0 +1,21 @@ +pub mod fiat_webhook_consumer; + +use std::error::Error; +use std::sync::Arc; + +use settings::Settings; +use storage::Database; +use streamer::{ConsumerStatusReporter, FiatWebhookPayload, QueueName, ShutdownReceiver, run_consumer}; + +use crate::consumers::{consumer_config, reader_for_queue}; + +use fiat_webhook_consumer::FiatWebhookConsumer; + +pub async fn run_consumer_fiat(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::FiatOrderWebhooks; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let consumer = FiatWebhookConsumer::new(database, settings.clone()); + let consumer_config = consumer_config(&settings.consumer); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await +} diff --git a/apps/daemon/src/consumers/fetch_address_transactions_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_address_transactions_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/fetch_address_transactions_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_address_transactions_consumer.rs diff --git a/apps/daemon/src/consumers/fetch_assets_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_assets_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/fetch_assets_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_assets_consumer.rs diff --git a/apps/daemon/src/consumers/fetch_blocks_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_blocks_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/fetch_blocks_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_blocks_consumer.rs diff --git a/apps/daemon/src/consumers/fetch_coin_addresses_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_coin_addresses_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/fetch_coin_addresses_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_coin_addresses_consumer.rs diff --git a/apps/daemon/src/consumers/fetch_nft_assets_addresses_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_nft_assets_addresses_consumer.rs similarity index 95% rename from apps/daemon/src/consumers/fetch_nft_assets_addresses_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_nft_assets_addresses_consumer.rs index d9c882b33..530d06ff1 100644 --- a/apps/daemon/src/consumers/fetch_nft_assets_addresses_consumer.rs +++ b/apps/daemon/src/consumers/indexer/fetch_nft_assets_addresses_consumer.rs @@ -12,6 +12,8 @@ use streamer::{ run_consumer, }; +use crate::consumers::reader_config; + pub struct FetchNftAssetsAddressesConsumer { #[allow(dead_code)] pub database: Database, @@ -34,7 +36,8 @@ impl FetchNftAssetsAddressesConsumer { ) -> Result<(), Box> { let queue = QueueName::FetchNftAssociations; let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(connection, settings.rabbitmq.prefetch).await?; + let config = reader_config(&settings.rabbitmq, name.clone()); + let stream_reader = StreamReader::from_connection(connection, config).await?; let stream_producer = StreamProducer::from_connection(connection).await?; let nft_config = NFTProviderConfig::new(settings.nft.opensea.key.secret.clone(), settings.nft.magiceden.key.secret.clone()); let nft_client = NFTClient::new(database.clone(), nft_config); diff --git a/apps/daemon/src/consumers/fetch_token_addresses_consumer.rs b/apps/daemon/src/consumers/indexer/fetch_token_addresses_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/fetch_token_addresses_consumer.rs rename to apps/daemon/src/consumers/indexer/fetch_token_addresses_consumer.rs diff --git a/apps/daemon/src/consumers/indexer/mod.rs b/apps/daemon/src/consumers/indexer/mod.rs new file mode 100644 index 000000000..f53049eaa --- /dev/null +++ b/apps/daemon/src/consumers/indexer/mod.rs @@ -0,0 +1,181 @@ +pub mod fetch_address_transactions_consumer; +pub mod fetch_assets_consumer; +pub mod fetch_blocks_consumer; +pub mod fetch_coin_addresses_consumer; +pub mod fetch_nft_assets_addresses_consumer; +pub mod fetch_token_addresses_consumer; + +use std::error::Error; +use std::sync::Arc; + +use cacher::CacherClient; +use primitives::{Chain, NFTChain}; +use settings::Settings; +use storage::Database; +use streamer::{ChainAddressPayload, ConsumerStatusReporter, FetchAssetsPayload, FetchBlocksPayload, QueueName, ShutdownReceiver, run_consumer}; + +use crate::consumers::runner::ChainConsumerRunner; +use crate::consumers::{chain_providers, consumer_config, reader_for_queue}; + +use fetch_address_transactions_consumer::FetchAddressTransactionsConsumer; +use fetch_assets_consumer::FetchAssetsConsumer; +use fetch_blocks_consumer::FetchBlocksConsumer; +use fetch_coin_addresses_consumer::FetchCoinAddressesConsumer; +use fetch_nft_assets_addresses_consumer::FetchNftAssetsAddressesConsumer; +use fetch_token_addresses_consumer::FetchTokenAddressesConsumer; + +pub async fn run_consumer_indexer(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let settings = Arc::new(settings); + + futures::future::try_join_all(vec![ + tokio::spawn(run_fetch_blocks(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_fetch_assets(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_fetch_token_associations(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_fetch_coin_associations(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_fetch_nft_associations(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_fetch_transaction_associations(settings.clone(), shutdown_rx.clone(), reporter.clone())), + ]) + .await?; + + Ok(()) +} + +async fn run_fetch_blocks(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + ChainConsumerRunner::new((*settings).clone(), QueueName::FetchBlocks, shutdown_rx, reporter) + .await? + .run(|runner, chain| async move { + let queue = QueueName::FetchBlocks; + let name = format!("{}.{}", queue, chain.as_ref()); + let stream_reader = runner.stream_reader().await?; + let stream_producer = runner.stream_producer().await?; + let consumer = FetchBlocksConsumer::new(chain_providers(&runner.settings, &name), stream_producer); + run_consumer::( + &name, + stream_reader, + queue, + Some(chain.as_ref()), + consumer, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} + +async fn run_fetch_assets(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::FetchAssets; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let cacher = CacherClient::new(&settings.redis.url).await; + let consumer = FetchAssetsConsumer { + providers: chain_providers(&settings, &name), + database, + cacher, + }; + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await +} + +async fn run_fetch_token_associations( + settings: Arc, + shutdown_rx: ShutdownReceiver, + reporter: Arc, +) -> Result<(), Box> { + ChainConsumerRunner::new((*settings).clone(), QueueName::FetchTokenAssociations, shutdown_rx, reporter) + .await? + .run(|runner, chain| async move { + let queue = QueueName::FetchTokenAssociations; + let name = format!("{}.{}", queue, chain.as_ref()); + let stream_reader = runner.stream_reader().await?; + let stream_producer = runner.stream_producer().await?; + let consumer = FetchTokenAddressesConsumer::new(chain_providers(&runner.settings, &name), runner.database, stream_producer, runner.cacher); + run_consumer::( + &name, + stream_reader, + queue, + Some(chain.as_ref()), + consumer, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} + +async fn run_fetch_coin_associations( + settings: Arc, + shutdown_rx: ShutdownReceiver, + reporter: Arc, +) -> Result<(), Box> { + ChainConsumerRunner::new((*settings).clone(), QueueName::FetchCoinAssociations, shutdown_rx, reporter) + .await? + .run(|runner, chain| async move { + let queue = QueueName::FetchCoinAssociations; + let name = format!("{}.{}", queue, chain.as_ref()); + let stream_reader = runner.stream_reader().await?; + let consumer = FetchCoinAddressesConsumer::new(chain_providers(&runner.settings, &name), runner.database, runner.cacher); + run_consumer::( + &name, + stream_reader, + queue, + Some(chain.as_ref()), + consumer, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} + +async fn run_fetch_nft_associations(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let chains: Vec = NFTChain::all().into_iter().map(Into::into).collect(); + ChainConsumerRunner::new((*settings).clone(), QueueName::FetchNftAssociations, shutdown_rx, reporter) + .await? + .run_for_chains(chains, |runner, chain| async move { + FetchNftAssetsAddressesConsumer::run( + runner.settings, + runner.database, + chain, + &runner.connection, + runner.cacher, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} + +async fn run_fetch_transaction_associations( + settings: Arc, + shutdown_rx: ShutdownReceiver, + reporter: Arc, +) -> Result<(), Box> { + ChainConsumerRunner::new((*settings).clone(), QueueName::FetchAddressTransactions, shutdown_rx, reporter) + .await? + .run(|runner, chain| async move { + let queue = QueueName::FetchAddressTransactions; + let name = format!("{}.{}", queue, chain.as_ref()); + let stream_reader = runner.stream_reader().await?; + let stream_producer = runner.stream_producer().await?; + let consumer = FetchAddressTransactionsConsumer::new(runner.database, chain_providers(&runner.settings, &name), stream_producer, runner.cacher); + run_consumer::( + &name, + stream_reader, + queue, + Some(chain.as_ref()), + consumer, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} diff --git a/apps/daemon/src/consumers/mod.rs b/apps/daemon/src/consumers/mod.rs index 373226d71..f0ea28446 100644 --- a/apps/daemon/src/consumers/mod.rs +++ b/apps/daemon/src/consumers/mod.rs @@ -1,63 +1,27 @@ -pub mod assets_addresses_consumer; -pub mod consumer_reporter; -pub mod fetch_address_transactions_consumer; -pub mod fetch_assets_consumer; -pub mod fetch_blocks_consumer; -pub mod fetch_coin_addresses_consumer; -pub mod fetch_nft_assets_addresses_consumer; -pub mod fetch_prices_consumer; -pub mod fetch_token_addresses_consumer; -pub mod nft; +pub mod fiat; +pub mod indexer; pub mod notifications; -pub mod rewards_consumer; -pub mod rewards_redemption_consumer; -pub mod store_charts_consumer; -pub mod store_prices_consumer; -pub mod store_transactions_consumer; -pub mod store_transactions_consumer_config; +pub mod prices; +pub mod rewards; +pub mod runner; +pub mod store; pub mod support; -use std::collections::HashMap; use std::error::Error; -use std::str::FromStr; -use std::sync::Arc; -pub use assets_addresses_consumer::AssetsAddressesConsumer; -use cacher::CacherClient; -pub use fetch_assets_consumer::FetchAssetsConsumer; -use pricer::PriceClient; -use primitives::ConfigKey; use settings::Settings; use settings_chain::ChainProviders; -use storage::{ConfigCacher, Database}; -pub use store_charts_consumer::StoreChartsConsumer; -pub use store_prices_consumer::StorePricesConsumer; -pub use store_transactions_consumer::StoreTransactionsConsumer; -pub use store_transactions_consumer_config::StoreTransactionsConsumerConfig; -use streamer::{ - AssetsAddressPayload, ChainAddressPayload, ChartsPayload, ConsumerConfig, ConsumerStatusReporter, FetchAssetsPayload, FetchBlocksPayload, FetchPricesPayload, - FiatWebhookPayload, InAppNotificationPayload, PricesPayload, QueueName, RewardsNotificationPayload, RewardsRedemptionPayload, ShutdownReceiver, StreamConnection, - StreamProducer, StreamProducerConfig, StreamReader, StreamReaderConfig, SupportWebhookPayload, TransactionsPayload, run_consumer, -}; +use streamer::{ConsumerConfig, QueueName, StreamProducer, StreamProducerConfig, StreamReader, StreamReaderConfig}; -use crate::consumers::{ - fetch_address_transactions_consumer::FetchAddressTransactionsConsumer, fetch_blocks_consumer::FetchBlocksConsumer, fetch_coin_addresses_consumer::FetchCoinAddressesConsumer, - fetch_nft_assets_addresses_consumer::FetchNftAssetsAddressesConsumer, fetch_prices_consumer::FetchPricesConsumer, fetch_token_addresses_consumer::FetchTokenAddressesConsumer, -}; -use crate::pusher::Pusher; -use crate::worker::pricer::price_updater::PriceUpdater; -use coingecko::CoinGeckoClient; -use gem_client::ReqwestClient; -use gem_evm::rpc::EthereumClient; -use gem_jsonrpc::JsonRpcClient; -use gem_rewards::{EvmClientProvider, TransferRedemptionService, WalletConfig}; -use primitives::rewards::RedemptionStatus; -use primitives::{Chain, ChainType, EVMChain}; -use settings::service_user_agent; -use settings_chain::ProviderFactory; +pub use fiat::run_consumer_fiat; +pub use indexer::run_consumer_indexer; +pub use prices::run_consumer_fetch_prices; +pub use rewards::run_consumer_rewards; +pub use store::run_consumer_store; +pub use support::run_consumer_support; pub fn chain_providers(settings: &Settings, name: &str) -> ChainProviders { - ChainProviders::from_settings(settings, &service_user_agent("consumer", Some(name))) + ChainProviders::from_settings(settings, &settings::service_user_agent("consumer", Some(name))) } pub(crate) fn consumer_config(consumer: &settings::Consumer) -> ConsumerConfig { @@ -68,405 +32,24 @@ pub(crate) fn consumer_config(consumer: &settings::Consumer) -> ConsumerConfig { } } -#[derive(Clone)] -struct ChainConsumerRunner { - settings: Settings, - database: Database, - connection: StreamConnection, - cacher: CacherClient, - config: ConsumerConfig, - shutdown_rx: ShutdownReceiver, - reporter: Arc, +pub(crate) fn reader_config(rabbitmq: &settings::RabbitMQ, name: String) -> StreamReaderConfig { + let retry = streamer::Retry::new(rabbitmq.retry.delay, rabbitmq.retry.timeout); + StreamReaderConfig::new(rabbitmq.url.clone(), name, rabbitmq.prefetch, retry) } -impl ChainConsumerRunner { - async fn new(settings: Settings, queue: QueueName, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let connection = StreamConnection::new(&settings.rabbitmq.url, queue.to_string()).await?; - let cacher = CacherClient::new(&settings.redis.url).await; - let config = consumer_config(&settings.consumer); - Ok(Self { - settings, - database, - connection, - cacher, - config, - shutdown_rx, - reporter, - }) - } - - async fn run(self, f: F) -> Result<(), Box> - where - F: Fn(Self, Chain) -> Fut + Clone + Send + 'static, - Fut: std::future::Future>> + Send + 'static, - { - let tasks: Vec<_> = Chain::all() - .into_iter() - .map(|chain| { - let runner = self.clone(); - let f = f.clone(); - tokio::spawn(async move { f(runner, chain).await }) - }) - .collect(); - - for result in futures::future::join_all(tasks).await { - result??; - } - Ok(()) - } -} - -pub async fn run_consumer_fetch_assets(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::FetchAssets; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let cacher = CacherClient::new(&settings.redis.url).await; - let consumer = FetchAssetsConsumer { - providers: chain_providers(&settings, &name), - database, - cacher, - }; - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await -} - -pub async fn run_consumer_store_transactions( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::StoreTransactions, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - let queue = QueueName::StoreTransactions; - let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(&runner.connection, runner.settings.rabbitmq.prefetch).await?; - let stream_producer = StreamProducer::from_connection(&runner.connection).await?; - let database = Database::new(&runner.settings.postgres.url, runner.settings.postgres.pool); - let consumer = StoreTransactionsConsumer { - database: database.clone(), - config_cacher: ConfigCacher::new(database.clone()), - stream_producer, - pusher: Pusher::new(database.clone()), - config: StoreTransactionsConsumerConfig {}, - }; - run_consumer::( - &name, - stream_reader, - queue, - Some(chain.as_ref()), - consumer, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_fetch_address_transactions( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::FetchAddressTransactions, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - let queue = QueueName::FetchAddressTransactions; - let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(&runner.connection, runner.settings.rabbitmq.prefetch).await?; - let stream_producer = StreamProducer::from_connection(&runner.connection).await?; - let consumer = FetchAddressTransactionsConsumer::new(runner.database, chain_providers(&runner.settings, &name), stream_producer, runner.cacher); - run_consumer::( - &name, - stream_reader, - queue, - Some(chain.as_ref()), - consumer, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_fetch_blocks(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::FetchBlocks, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - let queue = QueueName::FetchBlocks; - let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(&runner.connection, runner.settings.rabbitmq.prefetch).await?; - let stream_producer = StreamProducer::from_connection(&runner.connection).await?; - let consumer = FetchBlocksConsumer::new(chain_providers(&runner.settings, &name), stream_producer); - run_consumer::( - &name, - stream_reader, - queue, - Some(chain.as_ref()), - consumer, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_store_assets_associations( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::StoreAssetsAssociations; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let consumer = AssetsAddressesConsumer::new(database); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter) - .await -} - -pub async fn run_consumer_fetch_token_associations( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::FetchTokenAssociations, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - let queue = QueueName::FetchTokenAssociations; - let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(&runner.connection, runner.settings.rabbitmq.prefetch).await?; - let stream_producer = StreamProducer::from_connection(&runner.connection).await?; - let consumer = FetchTokenAddressesConsumer::new(chain_providers(&runner.settings, &name), runner.database, stream_producer, runner.cacher); - run_consumer::( - &name, - stream_reader, - queue, - Some(chain.as_ref()), - consumer, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_fetch_coin_associations( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::FetchCoinAssociations, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - let queue = QueueName::FetchCoinAssociations; - let name = format!("{}.{}", queue, chain.as_ref()); - let stream_reader = StreamReader::from_connection(&runner.connection, runner.settings.rabbitmq.prefetch).await?; - let consumer = FetchCoinAddressesConsumer::new(chain_providers(&runner.settings, &name), runner.database, runner.cacher); - run_consumer::( - &name, - stream_reader, - queue, - Some(chain.as_ref()), - consumer, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_fetch_nft_associations( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - ChainConsumerRunner::new(settings, QueueName::FetchNftAssociations, shutdown_rx, reporter) - .await? - .run(|runner, chain| async move { - FetchNftAssetsAddressesConsumer::run( - runner.settings, - runner.database, - chain, - &runner.connection, - runner.cacher, - runner.config, - runner.shutdown_rx, - runner.reporter, - ) - .await - }) - .await -} - -pub async fn run_consumer_support(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - use support::support_webhook_consumer::SupportWebhookConsumer; - let queue = QueueName::SupportWebhooks; +pub(crate) async fn reader_for_queue(settings: &Settings, queue: &QueueName) -> Result<(String, StreamReader), Box> { let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let consumer = SupportWebhookConsumer::new(&settings).await?; - let consumer_config = consumer_config(&settings.consumer); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await + let config = reader_config(&settings.rabbitmq, name.clone()); + let reader = StreamReader::new(config).await?; + Ok((name, reader)) } -pub async fn run_consumer_fiat(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - use crate::worker::fiat::fiat_webhook_consumer::FiatWebhookConsumer; - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::FiatOrderWebhooks; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let consumer = FiatWebhookConsumer::new(database, settings.clone()); - let consumer_config = consumer_config(&settings.consumer); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await -} - -pub async fn run_consumer_store_prices(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::StorePrices; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let cacher_client = CacherClient::new(&settings.redis.url).await; - let price_client = PriceClient::new(database.clone(), cacher_client); - let config = ConfigCacher::new(database.clone()); - let ttl_seconds = config.get_duration(ConfigKey::PriceOutdated)?.as_secs() as i64; - let consumer = StorePricesConsumer::new(database, price_client, ttl_seconds); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await -} - -pub async fn run_consumer_store_charts(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::StoreCharts; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let cacher_client = CacherClient::new(&settings.redis.url).await; - let price_client = PriceClient::new(database, cacher_client); - let consumer = StoreChartsConsumer::new(price_client); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await -} - -pub async fn run_consumer_rewards(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::RewardsEvents; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); - let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; - let consumer = rewards_consumer::RewardsConsumer::new(database, stream_producer); - let consumer_config = consumer_config(&settings.consumer); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await -} - -pub async fn run_consumer_in_app_notifications( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::NotificationsInApp; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); - let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; - let consumer = notifications::InAppNotificationsConsumer::new(database, stream_producer); - let consumer_config = consumer_config(&settings.consumer); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter) - .await -} - -pub async fn run_rewards_redemption_consumer( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let config = ConfigCacher::new(database.clone()); - let retry_config = rewards_redemption_consumer::RedemptionRetryConfig { - max_retries: config.get_i64(ConfigKey::RedemptionRetryMaxRetries)? as u32, - delay: config.get_duration(ConfigKey::RedemptionRetryDelay)?, - errors: config.get_vec_string(ConfigKey::RedemptionRetryErrors)?, - }; - let queue = QueueName::RewardsRedemptions; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); - let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; - let wallets = parse_rewards_wallets(&settings)?; - let client_provider = create_evm_client_provider(settings.clone()); - let redemption_service = Arc::new(TransferRedemptionService::new(wallets, client_provider)); - let consumer = rewards_redemption_consumer::RewardsRedemptionConsumer::new(database, redemption_service, retry_config, stream_producer); - let consumer_config = consumer_config(&settings.consumer); - run_consumer::, RedemptionStatus>( - &name, - stream_reader, - queue, - None, - consumer, - consumer_config, - shutdown_rx, - reporter, - ) - .await -} - -fn parse_rewards_wallets(settings: &Settings) -> Result, Box> { - let mut wallets = HashMap::new(); - - for (chain_type_name, wallet_config) in &settings.rewards.wallets { - let chain_type = ChainType::from_str(chain_type_name).map_err(|_| format!("Invalid chain type: {}", chain_type_name))?; - wallets.insert( - chain_type, - WalletConfig { - key: wallet_config.key.clone(), - address: wallet_config.address.clone(), - }, - ); - } - - Ok(wallets) +fn producer_config(settings: &Settings) -> StreamProducerConfig { + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry) } -fn create_evm_client_provider(settings: Settings) -> EvmClientProvider { - Arc::new(move |chain: EVMChain| { - let chain_config = ProviderFactory::get_chain_config(chain.to_chain(), &settings); - let reqwest_client = gem_client::builder().build().ok()?; - let client = ReqwestClient::new(chain_config.url.clone(), reqwest_client); - let rpc_client = JsonRpcClient::new(client); - Some(EthereumClient::new(rpc_client, chain)) - }) -} - -pub async fn run_consumer_fetch_prices(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let queue = QueueName::FetchPrices; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let cacher_client = CacherClient::new(&settings.redis.url).await; - let coingecko_client = CoinGeckoClient::new(&settings.coingecko.key.secret); - let price_client = PriceClient::new(database, cacher_client); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); - let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; - let price_updater = PriceUpdater::new(price_client, coingecko_client, stream_producer); - let consumer = FetchPricesConsumer::new(price_updater); - run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await +pub(crate) async fn producer_for_queue(settings: &Settings, name: &str) -> Result> { + let config = producer_config(settings); + StreamProducer::new(&config, name).await } diff --git a/apps/daemon/src/consumers/nft/collections_updater.rs b/apps/daemon/src/consumers/nft/collections_updater.rs deleted file mode 100644 index 837c759be..000000000 --- a/apps/daemon/src/consumers/nft/collections_updater.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::str::FromStr; - -use ::nft::provider::get_image_mime_type; -use ::nft::providers::opensea::{model::Collection, OpenSeaClient}; -use primitives::{Chain, LinkType}; -use storage::{Database, models::{nft_collection::UpdateNftCollectionImageUrl, NftCollection, NftLink}}; - -pub struct OpenSeaUpdater { - database: Database, - opensea_client: OpenSeaClient, -} - -impl OpenSeaUpdater { - pub fn new(database: Database, opensea_client: OpenSeaClient) -> Self { - - Self { database, opensea_client } - } - - pub async fn update_collections(&self) -> Result> { - let collections = self.database.nft()?.get_nft_collections_all()?; - - for collection in collections.clone() { - let chain = Chain::from_str(collection.chain.as_str())?; - match chain { - Chain::Ethereum => { - let opensea_collection = self.opensea_client.get_collection_id(chain.as_ref(), &collection.contract_address).await?; - let _ = self.update_collection(collection.clone(), opensea_collection); - - // update mime types - if collection.image_preview_mime_type.is_none() { - let image_preview_mime_type = get_image_mime_type(&collection.clone().image_preview_url.unwrap_or_default()).await?; - - let update = UpdateNftCollectionImageUrl { - id: collection.id.clone(), - image_preview_url: collection.image_preview_url.clone(), - image_preview_mime_type: Some(image_preview_mime_type), - }; - - self.database.nft()?.update_nft_collection_image_url(update)?; - } - - println!("Updating collection: {}", collection.name); - } - _ => continue, - } - } - - Ok(collections.len()) - } - - fn update_collection(&self, collection: NftCollection, opensea_collection: Collection) -> Result<(), Box> { - let mut links: Vec = vec![]; - - if !opensea_collection.opensea_url.is_empty() { - links.push(NftLink { - collection_id: collection.id.clone(), - link_type: LinkType::OpenSea.as_ref().to_string(), - url: opensea_collection.opensea_url, - }); - } - if !opensea_collection.project_url.is_empty() { - links.push(NftLink { - collection_id: collection.id.clone(), - link_type: LinkType::Website.as_ref().to_string(), - url: opensea_collection.project_url, - }); - } - if !opensea_collection.twitter_username.is_empty() { - links.push(NftLink { - collection_id: collection.id.clone(), - link_type: LinkType::X.as_ref().to_string(), - url: format!("https://x.com/{}", opensea_collection.twitter_username), - }); - } - if !opensea_collection.instagram_username.is_empty() { - links.push(NftLink { - collection_id: collection.id.clone(), - link_type: LinkType::Instagram.as_ref().to_string(), - url: format!("https://instagram.com/{}", opensea_collection.instagram_username), - }); - } - self.database.nft()?.add_nft_collections_links(links.clone())?; - Ok(()) - } -} diff --git a/apps/daemon/src/consumers/nft/mod.rs b/apps/daemon/src/consumers/nft/mod.rs deleted file mode 100644 index 0bbfb6ac9..000000000 --- a/apps/daemon/src/consumers/nft/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -mod nft_collection_asset_consumer; -mod nft_collection_consumer; - -use nft_collection_asset_consumer::UpdateNftCollectionAssetsConsumer; -use nft_collection_consumer::UpdateNftCollectionConsumer; - -use super::consumer_config; -use futures::future::try_join_all; -use settings::Settings; -use std::error::Error; -use std::sync::Arc; -use streamer::{ConsumerStatusReporter, FetchNFTCollectionAssetPayload, FetchNFTCollectionPayload, QueueName, ShutdownReceiver, StreamReader, StreamReaderConfig, run_consumer}; - -pub async fn run_consumer_nft_collections( - settings: Settings, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - let settings = Arc::new(settings); - - try_join_all(vec![ - tokio::spawn(run_collections_consumer(settings.clone(), shutdown_rx.clone(), reporter.clone())), - tokio::spawn(run_collection_assets_consumer(settings, shutdown_rx, reporter)), - ]) - .await?; - - Ok(()) -} - -async fn run_collections_consumer(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { - let queue = QueueName::FetchNFTCollection; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let consumer = UpdateNftCollectionConsumer::new(); - - run_consumer::( - "consume_nft_collections", - stream_reader, - queue, - None, - consumer, - consumer_config(&settings.consumer), - shutdown_rx, - reporter, - ) - .await -} - -async fn run_collection_assets_consumer( - settings: Arc, - shutdown_rx: ShutdownReceiver, - reporter: Arc, -) -> Result<(), Box> { - let queue = QueueName::FetchNFTCollectionAssets; - let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; - let consumer = UpdateNftCollectionAssetsConsumer::new(); - - run_consumer::( - "consume_nft_collection_assets", - stream_reader, - queue, - None, - consumer, - consumer_config(&settings.consumer), - shutdown_rx, - reporter, - ) - .await -} diff --git a/apps/daemon/src/consumers/nft/nft_collection_asset_consumer.rs b/apps/daemon/src/consumers/nft/nft_collection_asset_consumer.rs deleted file mode 100644 index feae65f23..000000000 --- a/apps/daemon/src/consumers/nft/nft_collection_asset_consumer.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::error::Error; - -use async_trait::async_trait; -use streamer::{FetchNFTCollectionAssetPayload, consumer::MessageConsumer}; - -pub struct UpdateNftCollectionAssetsConsumer {} - -impl UpdateNftCollectionAssetsConsumer { - pub fn new() -> Self { - Self {} - } -} - -#[async_trait] -impl MessageConsumer for UpdateNftCollectionAssetsConsumer { - async fn should_process(&self, _payload: FetchNFTCollectionAssetPayload) -> Result> { - Ok(true) - } - async fn process(&self, _payload: FetchNFTCollectionAssetPayload) -> Result> { - Ok(0) - } -} diff --git a/apps/daemon/src/consumers/nft/nft_collection_consumer.rs b/apps/daemon/src/consumers/nft/nft_collection_consumer.rs deleted file mode 100644 index b18f8cfa1..000000000 --- a/apps/daemon/src/consumers/nft/nft_collection_consumer.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::error::Error; - -use async_trait::async_trait; -use streamer::{FetchNFTCollectionPayload, consumer::MessageConsumer}; - -pub struct UpdateNftCollectionConsumer {} - -impl UpdateNftCollectionConsumer { - pub fn new() -> Self { - Self {} - } -} - -#[async_trait] -impl MessageConsumer for UpdateNftCollectionConsumer { - async fn should_process(&self, _payload: FetchNFTCollectionPayload) -> Result> { - Ok(true) - } - async fn process(&self, _payload: FetchNFTCollectionPayload) -> Result> { - Ok(0) - } -} diff --git a/apps/daemon/src/consumers/notifications/mod.rs b/apps/daemon/src/consumers/notifications/mod.rs index 575ed6bc1..d3ec85424 100644 --- a/apps/daemon/src/consumers/notifications/mod.rs +++ b/apps/daemon/src/consumers/notifications/mod.rs @@ -12,10 +12,12 @@ use std::error::Error; use std::sync::Arc; use storage::Database; use streamer::{ - ConsumerConfig, ConsumerStatusReporter, NotificationsFailedPayload, NotificationsPayload, QueueName, ShutdownReceiver, StreamProducer, StreamProducerConfig, StreamReader, - StreamReaderConfig, run_consumer, + ConsumerConfig, ConsumerStatusReporter, InAppNotificationPayload, NotificationsFailedPayload, NotificationsPayload, QueueName, ShutdownReceiver, StreamProducer, + StreamProducerConfig, StreamReader, run_consumer, }; +use crate::consumers::reader_config; + fn consumer_config(consumer: &settings::Consumer) -> ConsumerConfig { ConsumerConfig { timeout_on_error: consumer.error.timeout, @@ -67,6 +69,7 @@ pub async fn run(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Ar shutdown_rx.clone(), reporter.clone(), )), + tokio::spawn(run_in_app_notifications_consumer(settings.clone(), database.clone(), shutdown_rx.clone(), reporter.clone())), ]) .await?; @@ -80,10 +83,10 @@ async fn run_notification_consumer( reporter: Arc, ) -> Result<(), Box> { let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; + let stream_reader = StreamReader::new(reader_config(&settings.rabbitmq, name.clone())).await?; let pusher_client = PusherClient::new(settings.pusher.url.clone(), settings.pusher.ios.topic.clone()); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; let consumer = NotificationsConsumer::new(pusher_client, stream_producer); @@ -99,10 +102,27 @@ async fn run_notifications_failed_consumer( reporter: Arc, ) -> Result<(), Box> { let name = queue.to_string(); - let config = StreamReaderConfig::new(settings.rabbitmq.url.clone(), name.clone(), settings.rabbitmq.prefetch); - let stream_reader = StreamReader::new(config).await?; + let stream_reader = StreamReader::new(reader_config(&settings.rabbitmq, name.clone())).await?; let consumer = NotificationsFailedConsumer::new((*database).clone()); let consumer_config = consumer_config(&settings.consumer); run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await } + +async fn run_in_app_notifications_consumer( + settings: Arc, + database: Arc, + shutdown_rx: ShutdownReceiver, + reporter: Arc, +) -> Result<(), Box> { + let queue = QueueName::NotificationsInApp; + let name = queue.to_string(); + let stream_reader = StreamReader::new(reader_config(&settings.rabbitmq, name.clone())).await?; + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); + let stream_producer = StreamProducer::new(&rabbitmq_config, &name).await?; + let consumer = InAppNotificationsConsumer::new((*database).clone(), stream_producer); + + let consumer_config = consumer_config(&settings.consumer); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await +} diff --git a/apps/daemon/src/consumers/fetch_prices_consumer.rs b/apps/daemon/src/consumers/prices/fetch_prices_consumer.rs similarity index 93% rename from apps/daemon/src/consumers/fetch_prices_consumer.rs rename to apps/daemon/src/consumers/prices/fetch_prices_consumer.rs index 543c5ab95..964ee7cd3 100644 --- a/apps/daemon/src/consumers/fetch_prices_consumer.rs +++ b/apps/daemon/src/consumers/prices/fetch_prices_consumer.rs @@ -4,7 +4,7 @@ use std::error::Error; use streamer::FetchPricesPayload; use streamer::consumer::MessageConsumer; -use crate::worker::pricer::price_updater::PriceUpdater; +use crate::worker::prices::price_updater::PriceUpdater; pub struct FetchPricesConsumer { price_updater: PriceUpdater, diff --git a/apps/daemon/src/consumers/prices/mod.rs b/apps/daemon/src/consumers/prices/mod.rs new file mode 100644 index 000000000..4d3d624e6 --- /dev/null +++ b/apps/daemon/src/consumers/prices/mod.rs @@ -0,0 +1,37 @@ +pub mod fetch_prices_consumer; + +use std::error::Error; +use std::sync::Arc; + +use cacher::CacherClient; +use coingecko::CoinGeckoClient; +use pricer::PriceClient; +use settings::Settings; +use storage::Database; +use streamer::{ConsumerStatusReporter, FetchPricesPayload, QueueName, ShutdownReceiver, run_consumer}; + +use crate::consumers::{consumer_config, producer_for_queue, reader_for_queue}; +use crate::worker::prices::price_updater::PriceUpdater; + +pub async fn run_consumer_fetch_prices(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::FetchPrices; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let cacher_client = CacherClient::new(&settings.redis.url).await; + let coingecko_client = CoinGeckoClient::new(&settings.coingecko.key.secret); + let price_client = PriceClient::new(database, cacher_client); + let stream_producer = producer_for_queue(&settings, &name).await?; + let price_updater = PriceUpdater::new(price_client, coingecko_client, stream_producer); + let consumer = fetch_prices_consumer::FetchPricesConsumer::new(price_updater); + run_consumer::( + &name, + stream_reader, + queue, + None, + consumer, + consumer_config(&settings.consumer), + shutdown_rx, + reporter, + ) + .await +} diff --git a/apps/daemon/src/consumers/rewards/mod.rs b/apps/daemon/src/consumers/rewards/mod.rs new file mode 100644 index 000000000..789ef52f6 --- /dev/null +++ b/apps/daemon/src/consumers/rewards/mod.rs @@ -0,0 +1,98 @@ +pub mod rewards_consumer; +pub mod rewards_redemption_consumer; + +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; +use std::sync::Arc; + +use gem_client::ReqwestClient; +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::JsonRpcClient; +use gem_rewards::{EvmClientProvider, TransferRedemptionService, WalletConfig}; +use primitives::rewards::RedemptionStatus; +use primitives::{ChainType, ConfigKey, EVMChain}; +use settings::Settings; +use settings_chain::ProviderFactory; +use storage::{ConfigCacher, Database}; +use streamer::{ConsumerStatusReporter, QueueName, RewardsNotificationPayload, RewardsRedemptionPayload, ShutdownReceiver, run_consumer}; + +use crate::consumers::{consumer_config, producer_for_queue, reader_for_queue}; + +pub async fn run_consumer_rewards(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let settings = Arc::new(settings); + + futures::future::try_join_all(vec![ + tokio::spawn(run_rewards_events(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_rewards_redemptions(settings.clone(), shutdown_rx.clone(), reporter.clone())), + ]) + .await?; + + Ok(()) +} + +async fn run_rewards_events(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::RewardsEvents; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let stream_producer = producer_for_queue(&settings, &name).await?; + let consumer = rewards_consumer::RewardsConsumer::new(database, stream_producer); + let consumer_config = consumer_config(&settings.consumer); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await +} + +async fn run_rewards_redemptions(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let config = ConfigCacher::new(database.clone()); + let retry_config = rewards_redemption_consumer::RedemptionRetryConfig { + max_retries: config.get_i64(ConfigKey::RedemptionRetryMaxRetries)? as u32, + delay: config.get_duration(ConfigKey::RedemptionRetryDelay)?, + errors: config.get_vec_string(ConfigKey::RedemptionRetryErrors)?, + }; + let queue = QueueName::RewardsRedemptions; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let stream_producer = producer_for_queue(&settings, &name).await?; + let wallets = parse_rewards_wallets(&settings)?; + let client_provider = create_evm_client_provider((*settings).clone()); + let redemption_service = Arc::new(TransferRedemptionService::new(wallets, client_provider)); + let consumer = rewards_redemption_consumer::RewardsRedemptionConsumer::new(database, redemption_service, retry_config, stream_producer); + let consumer_config = consumer_config(&settings.consumer); + run_consumer::, RedemptionStatus>( + &name, + stream_reader, + queue, + None, + consumer, + consumer_config, + shutdown_rx, + reporter, + ) + .await +} + +fn parse_rewards_wallets(settings: &Settings) -> Result, Box> { + let mut wallets = HashMap::new(); + + for (chain_type_name, wallet_config) in &settings.rewards.wallets { + let chain_type = ChainType::from_str(chain_type_name).map_err(|_| format!("Invalid chain type: {}", chain_type_name))?; + wallets.insert( + chain_type, + WalletConfig { + key: wallet_config.key.clone(), + address: wallet_config.address.clone(), + }, + ); + } + + Ok(wallets) +} + +fn create_evm_client_provider(settings: Settings) -> EvmClientProvider { + Arc::new(move |chain: EVMChain| { + let chain_config = ProviderFactory::get_chain_config(chain.to_chain(), &settings); + let reqwest_client = gem_client::builder().build().ok()?; + let client = ReqwestClient::new(chain_config.url.clone(), reqwest_client); + let rpc_client = JsonRpcClient::new(client); + Some(EthereumClient::new(rpc_client, chain)) + }) +} diff --git a/apps/daemon/src/consumers/rewards_consumer.rs b/apps/daemon/src/consumers/rewards/rewards_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/rewards_consumer.rs rename to apps/daemon/src/consumers/rewards/rewards_consumer.rs diff --git a/apps/daemon/src/consumers/rewards_redemption_consumer.rs b/apps/daemon/src/consumers/rewards/rewards_redemption_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/rewards_redemption_consumer.rs rename to apps/daemon/src/consumers/rewards/rewards_redemption_consumer.rs diff --git a/apps/daemon/src/consumers/runner.rs b/apps/daemon/src/consumers/runner.rs new file mode 100644 index 000000000..ef22cd6d1 --- /dev/null +++ b/apps/daemon/src/consumers/runner.rs @@ -0,0 +1,85 @@ +use std::error::Error; +use std::sync::Arc; + +use cacher::CacherClient; +use gem_tracing::{error_with_fields, info_with_fields}; +use primitives::Chain; +use settings::Settings; +use storage::Database; +use streamer::{ConsumerConfig, ConsumerStatusReporter, ShutdownReceiver, StreamConnection, StreamProducer, StreamReader}; + +use crate::consumers::{consumer_config, reader_config}; + +#[derive(Clone)] +pub struct ChainConsumerRunner { + pub settings: Settings, + pub database: Database, + pub connection: StreamConnection, + pub cacher: CacherClient, + pub config: ConsumerConfig, + pub shutdown_rx: ShutdownReceiver, + pub reporter: Arc, + queue: streamer::QueueName, +} + +impl ChainConsumerRunner { + pub async fn new( + settings: Settings, + queue: streamer::QueueName, + shutdown_rx: ShutdownReceiver, + reporter: Arc, + ) -> Result> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let connection = StreamConnection::new(&settings.rabbitmq.url, queue.to_string()).await?; + let cacher = CacherClient::new(&settings.redis.url).await; + let config = consumer_config(&settings.consumer); + Ok(Self { + settings, + database, + connection, + cacher, + config, + shutdown_rx, + reporter, + queue, + }) + } + + pub async fn stream_reader(&self) -> Result> { + let config = reader_config(&self.settings.rabbitmq, self.connection.name().to_string()); + StreamReader::from_connection(&self.connection, config).await + } + + pub async fn stream_producer(&self) -> Result> { + StreamProducer::from_connection(&self.connection).await + } + + pub async fn run(self, f: F) -> Result<(), Box> + where + F: Fn(Self, Chain) -> Fut + Clone + Send + 'static, + Fut: std::future::Future>> + Send + 'static, + { + self.run_for_chains(Chain::all(), f).await + } + + pub async fn run_for_chains(self, chains: Vec, f: F) -> Result<(), Box> + where + F: Fn(Self, Chain) -> Fut + Clone + Send + 'static, + Fut: std::future::Future>> + Send + 'static, + { + info_with_fields!("running consumer", consumer = self.queue.to_string(), chains = chains.len()); + let tasks = chains.into_iter().map(|chain| { + let runner = self.clone(); + let f = f.clone(); + async move { (chain, f(runner, chain).await) } + }); + + for (chain, result) in futures::future::join_all(tasks).await { + if let Err(err) = result { + error_with_fields!("consumer chain error", &*err, chain = chain.as_ref()); + self.reporter.report_error(&self.queue.to_string(), &format!("{:?}", err)).await; + } + } + Ok(()) + } +} diff --git a/apps/daemon/src/consumers/store/mod.rs b/apps/daemon/src/consumers/store/mod.rs new file mode 100644 index 000000000..7d3c29cf7 --- /dev/null +++ b/apps/daemon/src/consumers/store/mod.rs @@ -0,0 +1,90 @@ +pub mod store_charts_consumer; +pub mod store_prices_consumer; +pub mod store_transactions_consumer; +pub mod store_transactions_consumer_config; + +pub use store_transactions_consumer::StoreTransactionsConsumer; +pub use store_transactions_consumer_config::StoreTransactionsConsumerConfig; + +use std::error::Error; +use std::sync::Arc; + +use cacher::CacherClient; +use pricer::PriceClient; +use primitives::ConfigKey; +use settings::Settings; +use storage::{ConfigCacher, Database}; +use streamer::{ChartsPayload, ConsumerStatusReporter, PricesPayload, QueueName, ShutdownReceiver, TransactionsPayload, run_consumer}; + +use crate::consumers::runner::ChainConsumerRunner; +use crate::consumers::{consumer_config, reader_for_queue}; +use crate::pusher::Pusher; + +use store_charts_consumer::StoreChartsConsumer; +use store_prices_consumer::StorePricesConsumer; + +pub async fn run_consumer_store(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let settings = Arc::new(settings); + + futures::future::try_join_all(vec![ + tokio::spawn(run_store_transactions(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_store_prices(settings.clone(), shutdown_rx.clone(), reporter.clone())), + tokio::spawn(run_store_charts(settings.clone(), shutdown_rx.clone(), reporter.clone())), + ]) + .await?; + + Ok(()) +} + +async fn run_store_transactions(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + ChainConsumerRunner::new((*settings).clone(), QueueName::StoreTransactions, shutdown_rx, reporter) + .await? + .run(|runner, chain| async move { + let queue = QueueName::StoreTransactions; + let name = format!("{}.{}", queue, chain.as_ref()); + let stream_reader = runner.stream_reader().await?; + let stream_producer = runner.stream_producer().await?; + let database = Database::new(&runner.settings.postgres.url, runner.settings.postgres.pool); + let consumer = StoreTransactionsConsumer { + database: database.clone(), + config_cacher: ConfigCacher::new(database.clone()), + stream_producer, + pusher: Pusher::new(database.clone()), + config: StoreTransactionsConsumerConfig {}, + }; + run_consumer::( + &name, + stream_reader, + queue, + Some(chain.as_ref()), + consumer, + runner.config, + runner.shutdown_rx, + runner.reporter, + ) + .await + }) + .await +} + +async fn run_store_prices(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::StorePrices; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let cacher_client = CacherClient::new(&settings.redis.url).await; + let price_client = PriceClient::new(database.clone(), cacher_client); + let config = ConfigCacher::new(database.clone()); + let ttl_seconds = config.get_duration(ConfigKey::PriceOutdated)?.as_secs() as i64; + let consumer = StorePricesConsumer::new(database, price_client, ttl_seconds); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await +} + +async fn run_store_charts(settings: Arc, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let database = Database::new(&settings.postgres.url, settings.postgres.pool); + let queue = QueueName::StoreCharts; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let cacher_client = CacherClient::new(&settings.redis.url).await; + let price_client = PriceClient::new(database, cacher_client); + let consumer = StoreChartsConsumer::new(price_client); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config(&settings.consumer), shutdown_rx, reporter).await +} diff --git a/apps/daemon/src/consumers/store_charts_consumer.rs b/apps/daemon/src/consumers/store/store_charts_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/store_charts_consumer.rs rename to apps/daemon/src/consumers/store/store_charts_consumer.rs diff --git a/apps/daemon/src/consumers/store_prices_consumer.rs b/apps/daemon/src/consumers/store/store_prices_consumer.rs similarity index 100% rename from apps/daemon/src/consumers/store_prices_consumer.rs rename to apps/daemon/src/consumers/store/store_prices_consumer.rs diff --git a/apps/daemon/src/consumers/store_transactions_consumer.rs b/apps/daemon/src/consumers/store/store_transactions_consumer.rs similarity index 87% rename from apps/daemon/src/consumers/store_transactions_consumer.rs rename to apps/daemon/src/consumers/store/store_transactions_consumer.rs index c9cfbc22e..e5c9621b2 100644 --- a/apps/daemon/src/consumers/store_transactions_consumer.rs +++ b/apps/daemon/src/consumers/store/store_transactions_consumer.rs @@ -3,10 +3,11 @@ use std::{collections::HashMap, error::Error}; use async_trait::async_trait; use primitives::{AssetIdVecExt, ConfigKey, Transaction, TransactionId}; -use storage::{AssetsRepository, ConfigCacher, Database, SubscriptionsRepository, TransactionsRepository}; +use storage::{AssetsAddressesRepository, AssetsRepository, ConfigCacher, Database, TransactionsRepository, WalletsRepository}; use streamer::{AssetId, AssetsAddressPayload, NotificationsPayload, StreamProducer, StreamProducerQueue, TransactionsPayload, consumer::MessageConsumer}; -use crate::{consumers::StoreTransactionsConsumerConfig, pusher::Pusher}; +use crate::consumers::store::StoreTransactionsConsumerConfig; +use crate::pusher::Pusher; const TRANSACTION_BATCH_SIZE: usize = 100; @@ -31,9 +32,9 @@ impl MessageConsumer for StoreTransactionsConsumer { let min_amount = self.config_cacher.get_f64(ConfigKey::TransactionsMinAmountUsd)?; let addresses: Vec<_> = transactions.iter().flat_map(|tx| tx.addresses()).collect::>().into_iter().collect(); - let subscriptions = self.database.subscriptions()?.get_subscriptions(chain, addresses)?; + let subscriptions = self.database.wallets()?.get_subscriptions_by_chain_addresses(chain, addresses)?; - let subscription_addresses: HashSet<_> = subscriptions.iter().map(|s| &s.subscription.address).collect(); + let subscription_addresses: HashSet<_> = subscriptions.iter().map(|s| &s.address).collect(); let asset_ids: Vec = transactions .iter() @@ -57,7 +58,7 @@ impl MessageConsumer for StoreTransactionsConsumer { for subscription in &subscriptions { for transaction in &transactions { - if !transaction.addresses().contains(&subscription.subscription.address) { + if !transaction.addresses().contains(&subscription.address) { continue; } @@ -74,7 +75,7 @@ impl MessageConsumer for StoreTransactionsConsumer { let assets_addresses = transaction .assets_addresses_with_fee() .into_iter() - .filter(|x| existing_assets_map.contains_key(&x.asset_id) && subscription.subscription.address == x.address) + .filter(|x| existing_assets_map.contains_key(&x.asset_id) && subscription.address == x.address) .collect::>(); address_assets_payload.push(AssetsAddressPayload::new(assets_addresses)); @@ -98,11 +99,7 @@ impl MessageConsumer for StoreTransactionsConsumer { .map(|asset_price| asset_price.asset.asset.clone()) .collect(); - if let Ok(notifications) = self - .pusher - .get_messages(subscription.device.clone(), transaction.clone(), subscription.subscription.clone(), assets) - .await - { + if let Ok(notifications) = self.pusher.get_messages(subscription, transaction.clone(), assets).await { notifications_payload.push(NotificationsPayload::new(notifications)); } } @@ -112,11 +109,12 @@ impl MessageConsumer for StoreTransactionsConsumer { let transactions_count = self.store_transactions(transactions_map.into_values().collect()).await?; let _ = self.stream_producer.publish_fetch_assets(fetch_assets_payload).await; let _ = self.stream_producer.publish_notifications_transactions(notifications_payload).await; + let assets_addresses: Vec<_> = address_assets_payload.into_iter().flat_map(|p| p.values).collect::>().into_iter().collect(); - let _ = self - .stream_producer - .publish_store_assets_addresses_associations(AssetsAddressPayload::new(assets_addresses)) - .await; + if !assets_addresses.is_empty() { + let _ = self.database.assets_addresses()?.add_assets_addresses(assets_addresses); + } + Ok(transactions_count) } } diff --git a/apps/daemon/src/consumers/store_transactions_consumer_config.rs b/apps/daemon/src/consumers/store/store_transactions_consumer_config.rs similarity index 100% rename from apps/daemon/src/consumers/store_transactions_consumer_config.rs rename to apps/daemon/src/consumers/store/store_transactions_consumer_config.rs diff --git a/apps/daemon/src/consumers/support/mod.rs b/apps/daemon/src/consumers/support/mod.rs index b3c68b432..9741e383a 100644 --- a/apps/daemon/src/consumers/support/mod.rs +++ b/apps/daemon/src/consumers/support/mod.rs @@ -1 +1,19 @@ pub mod support_webhook_consumer; + +use std::error::Error; +use std::sync::Arc; + +use settings::Settings; +use streamer::{ConsumerStatusReporter, QueueName, ShutdownReceiver, SupportWebhookPayload, run_consumer}; + +use crate::consumers::{consumer_config, reader_for_queue}; + +use support_webhook_consumer::SupportWebhookConsumer; + +pub async fn run_consumer_support(settings: Settings, shutdown_rx: ShutdownReceiver, reporter: Arc) -> Result<(), Box> { + let queue = QueueName::SupportWebhooks; + let (name, stream_reader) = reader_for_queue(&settings, &queue).await?; + let consumer = SupportWebhookConsumer::new(&settings).await?; + let consumer_config = consumer_config(&settings.consumer); + run_consumer::(&name, stream_reader, queue, None, consumer, consumer_config, shutdown_rx, reporter).await +} diff --git a/apps/daemon/src/consumers/support/support_webhook_consumer.rs b/apps/daemon/src/consumers/support/support_webhook_consumer.rs index 95adad9f2..c3e553e95 100644 --- a/apps/daemon/src/consumers/support/support_webhook_consumer.rs +++ b/apps/daemon/src/consumers/support/support_webhook_consumer.rs @@ -17,17 +17,18 @@ pub struct SupportWebhookConsumer { impl SupportWebhookConsumer { pub async fn new(settings: &Settings) -> Result> { let database = Database::new(&settings.postgres.url, settings.postgres.pool); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); let stream_producer = StreamProducer::new(&rabbitmq_config, "daemon_support_producer").await?; Ok(Self { support_client: SupportClient::new(database, stream_producer), }) } - async fn process_webhook(&self, device: &Device, support_device_id: &str, webhook: &ChatwootWebhookPayload) -> Result> { + async fn process_webhook(&self, device: &Device, webhook: &ChatwootWebhookPayload) -> Result> { match webhook.event.as_str() { - EVENT_MESSAGE_CREATED => self.support_client.handle_message_created(device, support_device_id, webhook).await, - EVENT_CONVERSATION_UPDATED | EVENT_CONVERSATION_STATUS_CHANGED => self.support_client.handle_conversation_updated(support_device_id, webhook).map(|_| 0), + EVENT_MESSAGE_CREATED => self.support_client.handle_message_created(device, webhook).await, + EVENT_CONVERSATION_UPDATED | EVENT_CONVERSATION_STATUS_CHANGED => self.support_client.handle_conversation_updated(webhook).map(|_| 0), _ => Ok(0), } } @@ -48,28 +49,23 @@ impl MessageConsumer for SupportWebhookConsumer { } }; - let Some(support_device_id) = webhook.get_support_device_id() else { - info_with_fields!("Support webhook missing support_device_id", event = webhook.event); + let Some(device_id) = webhook.get_device_id() else { + info_with_fields!("Support webhook missing device_id", event = webhook.event); return Ok(true); }; - let Some(device) = self.support_client.get_device(&support_device_id)? else { - info_with_fields!("Support webhook device not found", support_device_id = support_device_id); + let Some(device) = self.support_client.get_device(&device_id)? else { + info_with_fields!("Support webhook device not found", device_id = device_id); return Ok(true); }; - match self.process_webhook(&device, &support_device_id, &webhook).await { + match self.process_webhook(&device, &webhook).await { Ok(notifications) => { - info_with_fields!( - "Support webhook processed", - support_device_id = support_device_id, - event = webhook.event, - notifications = notifications - ); + info_with_fields!("Support webhook processed", device_id = device_id, event = webhook.event, notifications = notifications); Ok(true) } Err(error) => { - error_with_fields!("Support webhook failed", &*error, support_device_id = support_device_id, payload = payload.data.to_string()); + error_with_fields!("Support webhook failed", &*error, device_id = device_id, payload = payload.data.to_string()); Err(error) } } diff --git a/apps/daemon/src/health.rs b/apps/daemon/src/health.rs new file mode 100644 index 000000000..ce8f22217 --- /dev/null +++ b/apps/daemon/src/health.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use rocket::{State, get, http::Status, routes}; + +pub struct HealthState { + ready: AtomicBool, +} + +impl Default for HealthState { + fn default() -> Self { + Self::new() + } +} + +impl HealthState { + pub fn new() -> Self { + Self { ready: AtomicBool::new(false) } + } + + pub fn set_ready(&self) { + self.ready.store(true, Ordering::Relaxed); + } + + pub fn is_ready(&self) -> bool { + self.ready.load(Ordering::Relaxed) + } +} + +#[get("/health")] +fn health(state: &State>) -> Status { + if state.is_ready() { Status::Ok } else { Status::ServiceUnavailable } +} + +pub async fn run_server(state: Arc) { + let _ = rocket::build().manage(state).mount("/", routes![health]).launch().await; +} + +pub fn spawn_server() -> Arc { + let state = Arc::new(HealthState::new()); + tokio::spawn({ + let state = state.clone(); + async move { run_server(state).await } + }); + state +} diff --git a/apps/daemon/src/main.rs b/apps/daemon/src/main.rs index ca5acc472..2b13e334c 100644 --- a/apps/daemon/src/main.rs +++ b/apps/daemon/src/main.rs @@ -1,24 +1,26 @@ mod consumers; +mod health; mod model; mod parser; mod pusher; +mod reporters; mod setup; mod shutdown; mod worker; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; -use crate::consumers::consumer_reporter::CacherConsumerReporter; +use crate::reporters::consumer::ConsumerReporter; use crate::model::{ConsumerService, DaemonService, WorkerService}; use crate::shutdown::ShutdownReceiver; use crate::worker::context::WorkerContext; -use crate::worker::job_history::CacheJobSchedule; -use crate::worker::job_reporter::CacherJobReporter; +use crate::worker::job_schedule::CacherJobTracker; use crate::worker::runtime::WorkerRuntime; use cacher::CacherClient; use gem_tracing::{SentryConfig, SentryTracing, error_with_fields, info_with_fields}; use job_runner::{JobHandle, JobSchedule, JobStatusReporter}; +use std::sync::atomic::{AtomicBool, Ordering}; use streamer::ConsumerStatusReporter; #[tokio::main] @@ -50,87 +52,172 @@ pub async fn main() { let _ = setup::run_setup_dev(settings).await; } DaemonService::Worker(service) => { - run_worker_mode(settings, service).await; + let services = match service { + Some(worker) => vec![worker], + None => WorkerService::all(), + }; + run_worker_services(settings, &services).await; } DaemonService::Parser(chain) => { - parser::run(settings, chain).await.expect("Parser failed"); + let health_state = health::spawn_server(); + parser::run(settings, chain, health_state).await.expect("Parser failed"); } DaemonService::Consumer(service) => { - run_consumer_mode(settings, service).await.expect("Consumer failed"); + let services = match service { + Some(consumer) => vec![consumer], + None => ConsumerService::all(), + }; + run_consumer_services(settings, &services).await.expect("Consumer failed"); } } } -async fn run_worker_mode(settings: settings::Settings, service: WorkerService) { +async fn run_worker_services(settings: settings::Settings, services: &[WorkerService]) { + if services.is_empty() { + info_with_fields!("no worker services requested", status = "ok"); + return; + } + let settings = Arc::new(settings); let (shutdown_tx, shutdown_rx) = shutdown::channel(); let shutdown_timeout = settings.daemon.shutdown.timeout; let metrics_cacher = CacherClient::new(&settings.metrics.redis.url).await; - let reporter: Arc = Arc::new(CacherJobReporter::new(metrics_cacher.clone(), service.as_ref())); - let job_history: Arc = Arc::new(CacheJobSchedule::new(&metrics_cacher)); - let runtime = WorkerRuntime::new(reporter.clone(), job_history.clone()); + let database = storage::Database::new(&settings.postgres.url, settings.postgres.pool); + + let health_state = health::spawn_server(); let signal_handle = shutdown::spawn_signal_handler(shutdown_tx); - let database = storage::Database::new(&settings.postgres.url, settings.postgres.pool); - let worker_context = WorkerContext::new(settings.clone(), database.clone(), runtime.clone()); - - let worker_result: Result, Box> = match service { - WorkerService::Alerter => worker::alerter::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Pricer => worker::pricer::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::PricesDex => worker::prices_dex::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Fiat => worker::fiat::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Assets => worker::assets::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Version => worker::version::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Transaction => worker::transaction::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Device => worker::device::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Search => worker::search::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Scan => worker::scan::jobs(worker_context.clone(), shutdown_rx).await, - WorkerService::Rewards => worker::rewards::jobs(worker_context, shutdown_rx).await, - }; - - let services = match worker_result { - Ok(handles) => handles, - Err(err) => { - error_with_fields!("worker init failed", &*err, worker = service.as_ref()); - return; + let worker_jobs: Vec<_> = futures::future::join_all(services.iter().map(|service| { + let svc = *service; + let tracker = Arc::new(CacherJobTracker::new(metrics_cacher.clone(), service.as_ref())); + let reporter: Arc = tracker.clone(); + let schedule: Arc = tracker; + let runtime = WorkerRuntime::new(reporter, schedule); + let context = WorkerContext::new(settings.clone(), database.clone(), runtime); + let shutdown_rx = shutdown_rx.clone(); + async move { + match svc.run_jobs(context, shutdown_rx).await { + Ok(handles) => Some((svc, handles)), + Err(err) => { + error_with_fields!("worker init failed", &*err, worker = svc.as_ref()); + None + } + } } - }; + })) + .await + .into_iter() + .flatten() + .collect(); + + let job_count: usize = worker_jobs.iter().map(|(_, jobs)| jobs.len()).sum(); + health_state.set_ready(); + info_with_fields!("workers ready", workers = worker_jobs.len(), jobs = job_count); signal_handle.await.ok(); - let status_flags: Vec<_> = services - .iter() - .map(|job| (job.name().to_string(), job.status_flag(), job.is_finished())) - .collect(); - let initial_pending: Vec<_> = status_flags - .iter() - .filter_map(|(name, _, done)| if *done { None } else { Some(name.clone()) }) - .collect(); - if !initial_pending.is_empty() { - info_with_fields!( - "waiting for worker shutdown", - worker = service.as_ref(), - jobs = initial_pending.join(", ") - ); + + if worker_jobs.is_empty() { + info_with_fields!("no workers started", status = "ok"); + return; + } + + let status_tracks = collect_status_tracks(&worker_jobs); + log_pending_workers(&status_tracks, "waiting for worker shutdown"); + + let handles_only: Vec<_> = worker_jobs.into_iter().flat_map(|(_, jobs)| jobs.into_iter().map(JobHandle::into_handle)).collect(); + let completed = shutdown::wait_with_timeout(handles_only, shutdown_timeout).await; + + if !completed { + log_pending_workers(&status_tracks, "force-stopping unfinished jobs"); } - let handles_only = services.into_iter().map(JobHandle::into_handle).collect(); - let _ = shutdown::wait_with_timeout(handles_only, shutdown_timeout).await; + info_with_fields!("all workers stopped", status = "ok"); } -async fn run_consumer_mode(settings: settings::Settings, service: ConsumerService) -> Result<(), Box> { - let (shutdown_tx, shutdown_rx) = shutdown::channel(); +struct WorkerStatusTrack { + worker: WorkerService, + jobs: Vec<(String, Arc)>, +} - shutdown::spawn_signal_handler(shutdown_tx); +fn collect_status_tracks(handles: &[(WorkerService, Vec)]) -> Vec { + handles + .iter() + .map(|(worker, jobs)| WorkerStatusTrack { + worker: *worker, + jobs: jobs.iter().map(|job| (job.name().to_string(), job.status_flag())).collect(), + }) + .collect() +} + +fn log_pending_workers(tracks: &[WorkerStatusTrack], message: &str) { + for track in tracks { + let pending: Vec<_> = track + .jobs + .iter() + .filter_map(|(name, flag)| if flag.load(Ordering::Relaxed) { None } else { Some(name.clone()) }) + .collect(); + if pending.is_empty() { + continue; + } + info_with_fields!(message, worker = track.worker.as_ref(), jobs = pending.join(", ")); + } +} + +async fn run_consumer_services(settings: settings::Settings, services: &[ConsumerService]) -> Result<(), Box> { + if services.is_empty() { + info_with_fields!("no consumer services requested", status = "ok"); + return Ok(()); + } + + let settings = Arc::new(settings); + let (shutdown_tx, shutdown_rx) = shutdown::channel(); + let signal_handle = shutdown::spawn_signal_handler(shutdown_tx); let metrics_cacher = CacherClient::new(&settings.metrics.redis.url).await; - let reporter: Arc = Arc::new(CacherConsumerReporter::new(metrics_cacher)); - let result = run_consumer(settings, service, shutdown_rx, reporter).await; + let health_state = health::spawn_server(); + let reporter: Arc = Arc::new(ConsumerReporter::new(metrics_cacher)); + let failures = Arc::new(Mutex::new(Vec::new())); + + let handles: Vec<_> = services + .iter() + .map(|service| { + let svc = service.clone(); + let svc_name = svc.as_ref().to_string(); + let settings = settings.clone(); + let reporter = reporter.clone(); + let shutdown_rx = shutdown_rx.clone(); + let failures = failures.clone(); + tokio::spawn(async move { + match run_consumer((*settings.as_ref()).clone(), svc, shutdown_rx, reporter).await { + Ok(_) => info_with_fields!("consumer stopped", consumer = svc_name.as_str(), status = "ok"), + Err(err) => { + let message = err.to_string(); + error_with_fields!("consumer failed", &*err, consumer = svc_name.as_str()); + if let Ok(mut list) = failures.lock() { + list.push(format!("{}: {}", svc_name, message)); + } + } + } + }) + }) + .collect(); + + health_state.set_ready(); - info_with_fields!("consumer stopped", status = "ok"); - result + signal_handle.await.ok(); + futures::future::join_all(handles).await; + + match failures.lock() { + Ok(errors) if errors.is_empty() => { + info_with_fields!("all consumers stopped", status = "ok"); + Ok(()) + } + Ok(errors) => Err(errors.join(", ").into()), + Err(_) => Err("failed to inspect consumer results".into()), + } } async fn run_consumer( @@ -140,23 +227,12 @@ async fn run_consumer( reporter: Arc, ) -> Result<(), Box> { match service { - ConsumerService::FetchAddressTransactions => consumers::run_consumer_fetch_address_transactions(settings, shutdown_rx, reporter).await, - ConsumerService::StoreTransactions => consumers::run_consumer_store_transactions(settings, shutdown_rx, reporter).await, - ConsumerService::FetchBlocks => consumers::run_consumer_fetch_blocks(settings, shutdown_rx, reporter).await, - ConsumerService::FetchAssets => consumers::run_consumer_fetch_assets(settings, shutdown_rx, reporter).await, - ConsumerService::FetchTokenAssociations => consumers::run_consumer_fetch_token_associations(settings, shutdown_rx, reporter).await, - ConsumerService::FetchCoinAssociations => consumers::run_consumer_fetch_coin_associations(settings, shutdown_rx, reporter).await, - ConsumerService::StoreAssetsAssociations => consumers::run_consumer_store_assets_associations(settings, shutdown_rx, reporter).await, - ConsumerService::FetchNftAssociations => consumers::run_consumer_fetch_nft_associations(settings, shutdown_rx, reporter).await, + ConsumerService::Store => consumers::run_consumer_store(settings, shutdown_rx, reporter).await, + ConsumerService::Indexer => consumers::run_consumer_indexer(settings, shutdown_rx, reporter).await, ConsumerService::Notifications => consumers::notifications::run(settings, shutdown_rx, reporter).await, - ConsumerService::InAppNotifications => consumers::run_consumer_in_app_notifications(settings, shutdown_rx, reporter).await, ConsumerService::Rewards => consumers::run_consumer_rewards(settings, shutdown_rx, reporter).await, - ConsumerService::RewardsRedemptions => consumers::run_rewards_redemption_consumer(settings, shutdown_rx, reporter).await, ConsumerService::Support => consumers::run_consumer_support(settings, shutdown_rx, reporter).await, ConsumerService::Fiat => consumers::run_consumer_fiat(settings, shutdown_rx, reporter).await, - ConsumerService::StorePrices => consumers::run_consumer_store_prices(settings, shutdown_rx, reporter).await, - ConsumerService::StoreCharts => consumers::run_consumer_store_charts(settings, shutdown_rx, reporter).await, ConsumerService::FetchPrices => consumers::run_consumer_fetch_prices(settings, shutdown_rx, reporter).await, - ConsumerService::Nft => consumers::nft::run_consumer_nft_collections(settings, shutdown_rx, reporter).await, } } diff --git a/apps/daemon/src/model.rs b/apps/daemon/src/model.rs index 19e8bb921..69d6c069d 100644 --- a/apps/daemon/src/model.rs +++ b/apps/daemon/src/model.rs @@ -1,57 +1,54 @@ use primitives::Chain; use std::str::FromStr; -use strum::{AsRefStr, EnumString}; +use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator}; -#[derive(Debug, Clone, PartialEq, AsRefStr, EnumString)] +#[derive(Debug, Clone, PartialEq, AsRefStr, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ConsumerService { - FetchAddressTransactions, - StoreTransactions, - FetchBlocks, - FetchAssets, - FetchTokenAssociations, - FetchCoinAssociations, - StoreAssetsAssociations, - FetchNftAssociations, + Store, + Indexer, Notifications, - InAppNotifications, Rewards, - RewardsRedemptions, Support, Fiat, - StorePrices, - StoreCharts, FetchPrices, - Nft, } -#[derive(Debug, Clone, Copy, AsRefStr, EnumString, PartialEq, Eq)] +impl ConsumerService { + pub fn all() -> Vec { + Self::iter().collect() + } +} + +#[derive(Debug, Clone, Copy, AsRefStr, EnumString, EnumIter, PartialEq, Eq)] #[strum(serialize_all = "snake_case")] pub enum WorkerService { Alerter, - Pricer, - PricesDex, + Prices, Fiat, Assets, - Version, - Transaction, - Device, + System, Search, - Scan, Rewards, } +impl WorkerService { + pub fn all() -> Vec { + Self::iter().collect() + } +} + #[derive(Debug, Clone, AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum DaemonService { Setup, SetupDev, #[strum(serialize = "worker")] - Worker(WorkerService), + Worker(Option), #[strum(serialize = "parser")] Parser(Option), #[strum(serialize = "consumer")] - Consumer(ConsumerService), + Consumer(Option), } impl DaemonService { @@ -59,7 +56,8 @@ impl DaemonService { match self { DaemonService::Setup => "setup".to_owned(), DaemonService::SetupDev => "setup_dev".to_owned(), - DaemonService::Worker(name) => format!("worker {}", name.as_ref()), + DaemonService::Worker(Some(name)) => format!("worker {}", name.as_ref()), + DaemonService::Worker(None) => "worker all".to_owned(), DaemonService::Parser(chain) => { if let Some(chain) = chain { format!("parser {}", chain.as_ref()) @@ -67,7 +65,8 @@ impl DaemonService { "parser".to_owned() } } - DaemonService::Consumer(consumer) => format!("consumer {}", consumer.as_ref()), + DaemonService::Consumer(Some(consumer)) => format!("consumer {}", consumer.as_ref()), + DaemonService::Consumer(None) => "consumer all".to_owned(), } } } @@ -91,8 +90,11 @@ impl FromStr for DaemonService { Self::SETUP => Ok(DaemonService::Setup), Self::SETUP_DEV => Ok(DaemonService::SetupDev), Self::WORKER => { - let worker_str = parts.get(1).ok_or_else(|| "Worker service must be specified".to_string())?; - let worker = WorkerService::from_str(worker_str).map_err(|_| format!("Invalid worker: {}", worker_str))?; + let worker = if let Some(worker_str) = parts.get(1) { + Some(WorkerService::from_str(worker_str).map_err(|_| format!("Invalid worker: {}", worker_str))?) + } else { + None + }; Ok(DaemonService::Worker(worker)) } Self::PARSER => { @@ -104,8 +106,11 @@ impl FromStr for DaemonService { Ok(DaemonService::Parser(chain)) } Self::CONSUMER => { - let consumer_str = parts.get(1).ok_or_else(|| "Consumer service must be specified".to_string())?; - let consumer = ConsumerService::from_str(consumer_str).map_err(|_| format!("Invalid consumer: {}", consumer_str))?; + let consumer = if let Some(consumer_str) = parts.get(1) { + Some(ConsumerService::from_str(consumer_str).map_err(|_| format!("Invalid consumer: {}", consumer_str))?) + } else { + None + }; Ok(DaemonService::Consumer(consumer)) } _ => Err(format!("Unknown service: {}", name)), diff --git a/apps/daemon/src/parser/mod.rs b/apps/daemon/src/parser/mod.rs index 7670543f0..25958ef85 100644 --- a/apps/daemon/src/parser/mod.rs +++ b/apps/daemon/src/parser/mod.rs @@ -1,32 +1,35 @@ mod parser_options; mod parser_state; +mod plan; pub use parser_options::ParserOptions; use parser_state::ParserStateService; use std::{ - cmp, error::Error, + sync::Arc, time::{Duration, Instant}, }; -use cacher::{CacheKey, CacherClient}; +use cacher::CacherClient; use chain_traits::ChainTraits; +use crate::reporters::parser::ParserReporter; use gem_tracing::{DurationMs, error_with_fields, info_with_fields}; -use primitives::{Chain, ParserError, ParserStatus}; +use primitives::Chain; use settings::Settings; use std::str::FromStr; -use storage::Database; use streamer::{StreamProducer, StreamProducerConfig, StreamProducerQueue, TransactionsPayload}; use crate::shutdown::{self, ShutdownReceiver}; +use plan::{BlockPlan, BlockPlanKind, plan_next_block, should_reload_catchup, timeout_for_state}; +use storage::{Database, models::ParserStateRow}; pub struct Parser { chain: Chain, provider: Box, stream_producer: StreamProducer, state_service: ParserStateService, - cacher: CacherClient, + reporter: ParserReporter, options: ParserOptions, shutdown_rx: ShutdownReceiver, } @@ -41,13 +44,14 @@ impl Parser { shutdown_rx: ShutdownReceiver, ) -> Self { let chain = provider.get_chain(); - let state_service = ParserStateService::new(chain, database, cacher.clone()); + let state_service = ParserStateService::new(chain, database); + let reporter = ParserReporter::new(chain, cacher); Self { chain, provider, stream_producer, state_service, - cacher, + reporter, options, shutdown_rx, } @@ -57,155 +61,142 @@ impl Parser { *self.shutdown_rx.borrow() } - async fn report_error(&self, error: &str) { - let cache_key = CacheKey::ParserStatus(self.chain.as_ref()); - let key = cache_key.key(); - let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); - - let truncated = if error.len() > 200 { error[..200].to_string() } else { error.to_string() }; + async fn sleep_or_shutdown(&self, duration: Duration) -> bool { + shutdown::sleep_or_shutdown(duration, &self.shutdown_rx).await + } - if let Some(entry) = status.errors.iter_mut().find(|e| e.message == truncated) { - entry.count += 1; - entry.timestamp = timestamp; + async fn wait_if_disabled(&self, state: &ParserStateRow, timeout: Duration) -> bool { + if state.is_enabled { + true } else { - status.errors.push(ParserError { - message: truncated, - count: 1, - timestamp, - }); + self.sleep_or_shutdown(timeout).await; + false } + } - if let Err(e) = self.cacher.set_cached(cache_key, &status).await { - info_with_fields!("parser status report failed", chain = self.chain.as_ref(), error = format!("{:?}", e)); + async fn get_latest_block(&self, state: &ParserStateRow) -> Result> { + let latest_block = self.provider.get_block_latest_number().await? as i64; + let _ = self.state_service.set_latest_block(latest_block); + + if state.current_block == 0 { + let _ = self.state_service.set_current_block(latest_block); } + + Ok(latest_block) } - pub async fn start(&self) -> Result<(), Box> { - self.state_service.init().await?; + async fn execute_plan(&self, plan: BlockPlan, state: &ParserStateRow, timeout: Duration) -> Result> { + let start = Instant::now(); + let blocks_desc = format!("{:?}", plan.range.blocks); + + match plan.kind { + BlockPlanKind::Enqueue => { + self.stream_producer.publish_blocks(self.chain, &plan.range.blocks).await?; + let _ = self.state_service.set_current_block(plan.range.end_block); + + info_with_fields!( + "block add to queue", + chain = self.chain.as_ref(), + blocks = blocks_desc, + remaining = plan.range.remaining, + duration = DurationMs(start.elapsed()) + ); + return Ok(true); + } + BlockPlanKind::Parse => {} + } + + match self.parse_blocks(plan.range.blocks).await { + Ok(result) => { + let _ = self.state_service.set_current_block(plan.range.end_block); + + info_with_fields!( + "block complete", + chain = self.chain.as_ref(), + blocks = blocks_desc, + transactions = result, + remaining = plan.range.remaining, + duration = DurationMs(start.elapsed()) + ); + } + Err(err) => { + error_with_fields!("parser parse_block", &*err, chain = self.chain.as_ref(), blocks = blocks_desc); + self.reporter.error(&format!("block: {:?}", err)).await; + self.sleep_or_shutdown(timeout).await; + return Ok(false); + } + } - let mut last_persist = Instant::now(); + if should_reload_catchup(plan.range.remaining, self.options.catchup_reload_interval) { + return Ok(false); + } + if state.timeout_between_blocks > 0 && self.sleep_or_shutdown(Duration::from_millis(state.timeout_between_blocks as u64)).await { + return Ok(false); + } + + Ok(true) + } + async fn process_blocks(&self, timeout: Duration) -> Result<(), Box> { loop { if self.is_shutdown() { - info_with_fields!("shutdown requested", chain = self.chain.as_ref()); break; } - if last_persist.elapsed() >= self.options.persist_interval { - self.state_service.persist_state().await; - last_persist = Instant::now(); + let state = self.state_service.get_state()?; + + let Some(plan) = plan_next_block(&state, state.current_block, state.latest_block) else { + break; + }; + + if !self.execute_plan(plan, &state, timeout).await? { + break; + } + } + + Ok(()) + } + + pub async fn start(&self) -> Result<(), Box> { + loop { + if self.is_shutdown() { + info_with_fields!("shutdown requested", chain = self.chain.as_ref()); + break; } let state = self.state_service.get_state()?; - let timeout = cmp::max(Duration::from_millis(state.timeout_latest_block as u64), self.options.timeout); + let timeout = timeout_for_state(&state, self.options.min_check, self.options.max_check); - if !state.is_enabled { - if shutdown::sleep_or_shutdown(timeout, &self.shutdown_rx).await { - break; - } + if !self.wait_if_disabled(&state, timeout).await { continue; } - let current_block = self.state_service.get_current_block().await; - let next_current_block = current_block + state.await_blocks as i64; - - match self.provider.get_block_latest_number().await { - Ok(latest_block) => { - let latest_block_i64 = latest_block as i64; - let _ = self.state_service.set_latest_block(latest_block_i64).await; - - if current_block == 0 { - let _ = self.state_service.set_current_block(latest_block_i64).await; - } - - if next_current_block >= latest_block_i64 { - info_with_fields!( - "parser ahead", - chain = self.chain.as_ref(), - current_block = current_block, - latest_block = latest_block, - await_blocks = state.await_blocks - ); - if shutdown::sleep_or_shutdown(timeout, &self.shutdown_rx).await { - break; - } - continue; - } - } + let latest_block = match self.get_latest_block(&state).await { + Ok(block) => block, Err(err) => { error_with_fields!("parser latest_block", &*err, chain = self.chain.as_ref()); - if shutdown::sleep_or_shutdown(timeout * 5, &self.shutdown_rx).await { - break; - } + self.reporter.error(&format!("latest_block: {:?}", err)).await; + self.sleep_or_shutdown(self.options.error_interval).await; continue; } + }; + + if state.current_block + state.await_blocks as i64 >= latest_block { + info_with_fields!( + "parser ahead", + chain = self.chain.as_ref(), + current_block = state.current_block, + latest_block = latest_block, + await_blocks = state.await_blocks, + next_check = DurationMs(timeout) + ); + self.sleep_or_shutdown(timeout).await; + continue; } - loop { - if self.is_shutdown() { - break; - } - - let start = Instant::now(); - let current_block = self.state_service.get_current_block().await; - let latest_block = self.state_service.get_latest_block().await; - - let start_block = current_block + 1; - let end_block = cmp::min(start_block + state.parallel_blocks as i64 - 1, latest_block - state.await_blocks as i64); - let next_blocks: Vec = (start_block..=end_block).map(|b| b as u64).collect(); - let remaining = latest_block - end_block - state.await_blocks as i64; - - if next_blocks.is_empty() { - break; - } - - if let Some(queue_behind_blocks) = state.queue_behind_blocks - && remaining > queue_behind_blocks as i64 - { - self.stream_producer.publish_blocks(self.chain, &next_blocks).await?; - let _ = self.state_service.set_current_block(end_block).await; - - info_with_fields!( - "block add to queue", - chain = self.chain.as_ref(), - blocks = format!("{:?}", next_blocks), - remaining = remaining, - duration = DurationMs(start.elapsed()) - ); - continue; - } - - match self.parse_blocks(next_blocks.clone()).await { - Ok(result) => { - let _ = self.state_service.set_current_block(end_block).await; - - info_with_fields!( - "block complete", - chain = self.chain.as_ref(), - blocks = format!("{:?}", next_blocks), - transactions = result, - remaining = remaining, - duration = DurationMs(start.elapsed()) - ); - } - Err(err) => { - error_with_fields!("parser parse_block", &*err, chain = self.chain.as_ref(), blocks = format!("{:?}", next_blocks)); - shutdown::sleep_or_shutdown(timeout, &self.shutdown_rx).await; - break; - } - } - - if remaining % self.options.catchup_reload_interval == 0 { - break; - } - if state.timeout_between_blocks > 0 && shutdown::sleep_or_shutdown(Duration::from_millis(state.timeout_between_blocks as u64), &self.shutdown_rx).await { - break; - } - } + self.process_blocks(timeout).await?; } - self.state_service.persist_state().await; info_with_fields!("parser stopped", chain = self.chain.as_ref()); Ok(()) @@ -223,13 +214,15 @@ impl Parser { } } -pub async fn run(settings: Settings, chain: Option) -> Result<(), Box> { +pub async fn run(settings: Settings, chain: Option, health_state: Arc) -> Result<(), Box> { let database = Database::new(&settings.postgres.url, settings.postgres.pool); let cacher = CacherClient::new(&settings.redis.url).await; let config = storage::ConfigCacher::new(database.clone()); let catchup_reload_interval = config.get_i64(primitives::ConfigKey::ParserCatchupReloadInterval)?; - let persist_interval = config.get_duration(primitives::ConfigKey::ParserPersistInterval)?; + let min_check = config.get_duration(primitives::ConfigKey::ParserMinCheckInterval)?; + let max_check = config.get_duration(primitives::ConfigKey::ParserMaxCheckInterval)?; + let error_interval = config.get_duration(primitives::ConfigKey::ParserErrorInterval)?; let chains: Vec = if let Some(chain) = chain { vec![chain] @@ -259,13 +252,16 @@ pub async fn run(settings: Settings, chain: Option) -> Result<(), Box) -> Result<(), Box Self { - Self { chain, database, cacher } - } - - pub async fn init(&self) -> Result<(), Box> { - let state = self.get_state()?; - let current_key = CacheKey::ParserCurrentBlock(self.chain.as_ref()); - let latest_key = CacheKey::ParserLatestBlock(self.chain.as_ref()); - - if self.cacher.get_i64(¤t_key.key()).await?.is_none() { - self.cacher.set_i64(¤t_key.key(), state.current_block, current_key.ttl()).await?; - } - if self.cacher.get_i64(&latest_key.key()).await?.is_none() { - self.cacher.set_i64(&latest_key.key(), state.latest_block, latest_key.ttl()).await?; - } - Ok(()) + pub fn new(chain: Chain, database: Database) -> Self { + Self { chain, database } } pub fn get_state(&self) -> Result> { Ok(self.database.parser_state()?.get_parser_state(self.chain.as_ref())?) } - pub async fn get_current_block(&self) -> i64 { - let key = CacheKey::ParserCurrentBlock(self.chain.as_ref()); - self.cacher.get_i64(&key.key()).await.unwrap_or(None).unwrap_or(0) - } - - pub async fn get_latest_block(&self) -> i64 { - let key = CacheKey::ParserLatestBlock(self.chain.as_ref()); - self.cacher.get_i64(&key.key()).await.unwrap_or(None).unwrap_or(0) - } - - pub async fn set_current_block(&self, block: i64) -> Result<(), Box> { - let key = CacheKey::ParserCurrentBlock(self.chain.as_ref()); - self.cacher.set_i64(&key.key(), block, key.ttl()).await - } - - pub async fn set_latest_block(&self, block: i64) -> Result<(), Box> { - let key = CacheKey::ParserLatestBlock(self.chain.as_ref()); - self.cacher.set_i64(&key.key(), block, key.ttl()).await + pub fn set_current_block(&self, block: i64) -> Result<(), Box> { + self.database.parser_state()?.set_parser_state_current_block(self.chain.as_ref(), block)?; + Ok(()) } - pub async fn persist_state(&self) { - let current_key = CacheKey::ParserCurrentBlock(self.chain.as_ref()); - let latest_key = CacheKey::ParserLatestBlock(self.chain.as_ref()); - - if let Ok(Some(current)) = self.cacher.get_i64(¤t_key.key()).await { - let _ = self - .database - .parser_state() - .ok() - .and_then(|mut c| c.set_parser_state_current_block(self.chain.as_ref(), current).ok()); - } - if let Ok(Some(latest)) = self.cacher.get_i64(&latest_key.key()).await { - let _ = self - .database - .parser_state() - .ok() - .and_then(|mut c| c.set_parser_state_latest_block(self.chain.as_ref(), latest).ok()); - } + pub fn set_latest_block(&self, block: i64) -> Result<(), Box> { + self.database.parser_state()?.set_parser_state_latest_block(self.chain.as_ref(), block)?; + Ok(()) } } diff --git a/apps/daemon/src/parser/plan.rs b/apps/daemon/src/parser/plan.rs new file mode 100644 index 000000000..3c48d0446 --- /dev/null +++ b/apps/daemon/src/parser/plan.rs @@ -0,0 +1,159 @@ +use std::cmp; +use std::time::Duration; + +use chrono::Utc; +use storage::models::ParserStateRow; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum BlockPlanKind { + Enqueue, + Parse, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct BlockRange { + pub blocks: Vec, + pub end_block: i64, + pub remaining: i64, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct BlockPlan { + pub range: BlockRange, + pub kind: BlockPlanKind, +} + +pub fn timeout_for_state(state: &ParserStateRow, min_check: Duration, max_check: Duration) -> Duration { + let block_time = Duration::from_millis(state.block_time as u64); + if block_time.is_zero() { + return cmp::max(Duration::from_millis(state.timeout_latest_block as u64), min_check); + } + + let elapsed = Utc::now().naive_utc().signed_duration_since(state.updated_at).num_milliseconds().max(0) as u64; + + let remaining = block_time.saturating_sub(Duration::from_millis(elapsed)); + let upper = cmp::max(cmp::min(block_time, max_check), min_check); + remaining.clamp(min_check, upper) +} + +pub fn should_reload_catchup(remaining: i64, interval: i64) -> bool { + interval > 0 && remaining % interval == 0 +} + +pub fn plan_next_block(state: &ParserStateRow, current_block: i64, latest_block: i64) -> Option { + let start_block = current_block + 1; + let end_block = cmp::min(start_block + state.parallel_blocks as i64 - 1, latest_block - state.await_blocks as i64); + if end_block < start_block { + return None; + } + let blocks = (start_block..=end_block).map(|b| b as u64).collect::>(); + let remaining = latest_block - end_block - state.await_blocks as i64; + let kind = if let Some(queue_behind_blocks) = state.queue_behind_blocks + && remaining > queue_behind_blocks as i64 + { + BlockPlanKind::Enqueue + } else { + BlockPlanKind::Parse + }; + + Some(BlockPlan { + range: BlockRange { blocks, end_block, remaining }, + kind, + }) +} + +#[cfg(test)] +mod tests { + use super::{BlockPlanKind, plan_next_block, should_reload_catchup, timeout_for_state}; + use chrono::{NaiveDateTime, Utc}; + use std::time::Duration; + use storage::models::ParserStateRow; + + fn state(await_blocks: i32, parallel_blocks: i32, timeout_latest_block: i32, queue_behind_blocks: Option) -> ParserStateRow { + ParserStateRow { + chain: "ethereum".to_string(), + current_block: 0, + latest_block: 0, + await_blocks, + timeout_between_blocks: 0, + timeout_latest_block, + parallel_blocks, + is_enabled: true, + updated_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(), + queue_behind_blocks, + block_time: 0, + } + } + + const MIN: Duration = Duration::from_secs(1); + const MAX: Duration = Duration::from_secs(8); + + #[test] + fn test_timeout_for_state_no_block_time() { + let s = state(1, 1, 500, None); + assert_eq!(timeout_for_state(&s, MIN, MAX), MIN); + assert_eq!(timeout_for_state(&s, Duration::from_millis(100), MAX), Duration::from_millis(500)); + } + + #[test] + fn test_timeout_for_state_uses_remaining_block_time() { + let mut s = state(1, 1, 0, None); + s.block_time = 12_000; + s.updated_at = Utc::now().naive_utc() - chrono::Duration::seconds(4); + let timeout = timeout_for_state(&s, MIN, MAX); + assert!(timeout >= Duration::from_secs(7) && timeout <= Duration::from_secs(9)); + } + + #[test] + fn test_timeout_for_state_caps_at_max() { + let mut s = state(1, 1, 0, None); + s.block_time = 600_000; + s.updated_at = Utc::now().naive_utc(); + assert_eq!(timeout_for_state(&s, MIN, MAX), MAX); + } + + #[test] + fn test_timeout_for_state_overdue_block() { + let mut s = state(1, 1, 0, None); + s.block_time = 10_000; + s.updated_at = Utc::now().naive_utc() - chrono::Duration::seconds(15); + assert_eq!(timeout_for_state(&s, MIN, MAX), MIN); + } + + #[test] + fn test_should_reload_catchup_respects_interval() { + assert!(!should_reload_catchup(10, 0)); + assert!(should_reload_catchup(10, 5)); + assert!(!should_reload_catchup(11, 5)); + } + + #[test] + fn test_plan_next_block_returns_none_when_no_blocks() { + let state = state(5, 3, 0, None); + let plan = plan_next_block(&state, 10, 12); + assert!(plan.is_none()); + } + + #[test] + fn test_plan_next_block_builds_expected_blocks() { + let state = state(1, 3, 0, None); + let plan = plan_next_block(&state, 5, 10).unwrap(); + assert_eq!(plan.range.blocks, vec![6, 7, 8]); + assert_eq!(plan.range.end_block, 8); + assert_eq!(plan.range.remaining, 1); + if let BlockPlanKind::Parse = plan.kind { + } else { + panic!("expected parse plan"); + } + } + + #[test] + fn test_plan_next_block_enqueues_when_behind() { + let state = state(1, 3, 0, Some(2)); + let plan = plan_next_block(&state, 5, 20).unwrap(); + if let BlockPlanKind::Enqueue = plan.kind { + } else { + panic!("expected enqueue plan"); + } + } +} diff --git a/apps/daemon/src/pusher/pusher.rs b/apps/daemon/src/pusher/pusher.rs index 1cceb968f..6560a5edf 100644 --- a/apps/daemon/src/pusher/pusher.rs +++ b/apps/daemon/src/pusher/pusher.rs @@ -3,8 +3,8 @@ use std::error::Error; use localizer::LanguageLocalizer; use number_formatter::BigNumberFormatter; use primitives::{ - AddressFormatter, Asset, AssetVecExt, Chain, GorushNotification, NFTAssetId, PushNotification, PushNotificationTransaction, PushNotificationTypes, Subscription, Transaction, - TransactionNFTTransferMetadata, TransactionSwapMetadata, TransactionType, + AddressFormatter, Asset, AssetVecExt, Chain, DeviceSubscription, GorushNotification, NFTAssetId, PushNotification, PushNotificationTransaction, PushNotificationTypes, + Transaction, TransactionNFTTransferMetadata, TransactionSwapMetadata, TransactionType, }; use storage::{Database, ScanAddressesRepository}; @@ -27,7 +27,7 @@ impl Pusher { } } - pub fn message(&self, localizer: LanguageLocalizer, transaction: Transaction, subscription: Subscription, assets: Vec) -> Result> { + pub fn message(&self, localizer: LanguageLocalizer, transaction: Transaction, address: &str, assets: Vec) -> Result> { let asset = assets.asset_result(transaction.asset_id.clone())?; let amount = BigNumberFormatter::value(transaction.value.as_str(), asset.decimals).unwrap_or_default(); let chain = transaction.asset_id.chain; @@ -37,7 +37,7 @@ impl Pusher { match transaction.transaction_type { TransactionType::Transfer | TransactionType::SmartContractCall => { - let is_sent = transaction.is_sent(subscription.address.clone()); + let is_sent = transaction.is_sent(address.to_string()); let value = self.get_value(amount, asset.symbol.clone()); let title = localizer.notification_transfer_title(is_sent, value.as_str()); let message = localizer.notification_transfer_description(is_sent, to_address.as_str(), from_address.as_str()); @@ -54,7 +54,7 @@ impl Pusher { } else { format!("#{}...", nft_asset_id.token_id.get(..6).unwrap_or(&nft_asset_id.token_id)) }; - let is_sent = transaction.is_sent(subscription.address.clone()); + let is_sent = transaction.is_sent(address.to_string()); let title = localizer.notification_nft_transfer_title(is_sent, &name); let message = localizer.notification_transfer_description(is_sent, to_address.as_str(), from_address.as_str()); Ok(Message { title, message: Some(message) }) @@ -100,14 +100,12 @@ impl Pusher { }) } TransactionType::PerpetualOpenPosition => { - let _is_sent = transaction.is_sent(subscription.address.clone()); let value = self.get_value(amount, asset.symbol.clone()); let title = format!("Opened Perpetual Position: {value}"); let message = format!("Opened perpetual position for {value} at {to_address}"); Ok(Message { title, message: Some(message) }) } TransactionType::PerpetualClosePosition => { - let _is_sent = transaction.is_sent(subscription.address.clone()); let value = self.get_value(amount, asset.symbol.clone()); let title = format!("Closed Perpetual Position: {value}"); let message = format!("Closed perpetual position for {value} at {to_address}"); @@ -132,24 +130,21 @@ impl Pusher { pub async fn get_messages( &self, - device: primitives::Device, + subscription: &DeviceSubscription, transaction: Transaction, - subscription: Subscription, assets: Vec, ) -> Result, Box> { - if !device.can_receive_push_notification() { + if !subscription.device.can_receive_push_notification() { return Ok(vec![]); } let transaction = transaction.finalize(vec![subscription.address.clone()]).without_utxo(); - let localizer = LanguageLocalizer::new_with_language(device.locale.as_str()); - let message = self.message(localizer, transaction.clone(), subscription.clone(), assets.clone())?; + let localizer = LanguageLocalizer::new_with_language(subscription.device.locale.as_str()); + let message = self.message(localizer, transaction.clone(), &subscription.address, assets.clone())?; - // TODO: Pass wallet_id from subscription once v2 subscriptions migration is complete let notification_transaction = PushNotificationTransaction { - wallet_index: Some(subscription.wallet_index), - wallet_id: String::new(), + wallet_id: subscription.wallet_id.id(), transaction_id: transaction.id.to_string(), transaction: transaction.clone(), asset_id: transaction.asset_id.to_string(), @@ -159,7 +154,7 @@ impl Pusher { data: serde_json::to_value(¬ification_transaction).ok(), }; - let notification = GorushNotification::from_device(device, message.title, message.message.unwrap_or_default(), data); + let notification = GorushNotification::from_device(subscription.device.clone(), message.title, message.message.unwrap_or_default(), data); Ok(vec![notification]) } diff --git a/apps/daemon/src/reporters/consumer.rs b/apps/daemon/src/reporters/consumer.rs new file mode 100644 index 000000000..450b3b33f --- /dev/null +++ b/apps/daemon/src/reporters/consumer.rs @@ -0,0 +1,47 @@ +use std::time::SystemTime; + +use async_trait::async_trait; +use cacher::{CacheKey, CacherClient}; +use primitives::ConsumerStatus; +use streamer::ConsumerStatusReporter; + +pub struct ConsumerReporter { + cacher: CacherClient, +} + +impl ConsumerReporter { + pub fn new(cacher: CacherClient) -> Self { + Self { cacher } + } +} + +#[async_trait] +impl ConsumerStatusReporter for ConsumerReporter { + async fn report_success(&self, name: &str, duration: u64, result: &str) { + let cache_key = CacheKey::ConsumerStatus(name); + let key = cache_key.key(); + let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); + let timestamp = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + + status.total_processed += 1; + status.last_success = Some(timestamp); + status.last_result = Some(result.to_string()); + + let prev_total = status.total_processed - 1; + status.avg_duration = (status.avg_duration * prev_total + duration) / status.total_processed; + + let _ = self.cacher.set_cached(cache_key, &status).await; + } + + async fn report_error(&self, name: &str, error: &str) { + let cache_key = CacheKey::ConsumerStatus(name); + let key = cache_key.key(); + let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); + + status.total_errors += 1; + + super::record_error(&mut status.errors, error); + + let _ = self.cacher.set_cached(cache_key, &status).await; + } +} diff --git a/apps/daemon/src/reporters/mod.rs b/apps/daemon/src/reporters/mod.rs new file mode 100644 index 000000000..02aed3995 --- /dev/null +++ b/apps/daemon/src/reporters/mod.rs @@ -0,0 +1,21 @@ +pub mod consumer; +pub mod parser; + +use primitives::ReportedError; +use std::time::SystemTime; + +fn record_error(errors: &mut Vec, error: &str) { + let timestamp = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + let message = if error.len() > 200 { &error[..200] } else { error }; + + if let Some(entry) = errors.iter_mut().find(|e| e.message == message) { + entry.count += 1; + entry.timestamp = timestamp; + } else { + errors.push(ReportedError { + message: message.to_string(), + count: 1, + timestamp, + }); + } +} diff --git a/apps/daemon/src/reporters/parser.rs b/apps/daemon/src/reporters/parser.rs new file mode 100644 index 000000000..28d16d9a0 --- /dev/null +++ b/apps/daemon/src/reporters/parser.rs @@ -0,0 +1,23 @@ +use cacher::{CacheKey, CacherClient}; +use primitives::{Chain, ParserStatus}; + +pub struct ParserReporter { + chain: Chain, + cacher: CacherClient, +} + +impl ParserReporter { + pub fn new(chain: Chain, cacher: CacherClient) -> Self { + Self { chain, cacher } + } + + pub async fn error(&self, error: &str) { + let cache_key = CacheKey::ParserStatus(self.chain.as_ref()); + let key = cache_key.key(); + let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); + + super::record_error(&mut status.errors, error); + + let _ = self.cacher.set_cached(cache_key, &status).await; + } +} diff --git a/apps/daemon/src/setup/mod.rs b/apps/daemon/src/setup/mod.rs index 8fcede2ab..c8cef8fba 100644 --- a/apps/daemon/src/setup/mod.rs +++ b/apps/daemon/src/setup/mod.rs @@ -1,7 +1,7 @@ use gem_tracing::info_with_fields; use prices_dex::PriceFeedProvider; use primitives::{ - Asset, AssetId, AssetTag, Chain, ConfigKey, FiatProviderName, NotificationType, PlatformStore as PrimitivePlatformStore, PriceAlert, PriceAlertDirection, Subscription, + Asset, AssetId, AssetTag, Chain, ConfigKey, FiatProviderName, NFTChain, NotificationType, PlatformStore as PrimitivePlatformStore, PriceAlert, PriceAlertDirection, }; use search_index::{INDEX_CONFIGS, INDEX_PRIMARY_KEY, SearchIndexClient}; use settings::Settings; @@ -10,20 +10,28 @@ use storage::models::{ConfigRow, FiatAssetRow, FiatProviderCountryRow, FiatRateR use storage::sql_types::{Platform, PlatformStore}; use storage::{ AssetsRepository, ChainsRepository, ConfigRepository, DevicesRepository, MigrationsRepository, NewNotificationRow, NewWalletRow, NotificationsRepository, - PriceAlertsRepository, PricesDexRepository, ReleasesRepository, RewardsRepository, SubscriptionsRepository, TagRepository, WalletSource, WalletType, WalletsRepository, + PriceAlertsRepository, PricesDexRepository, ReleasesRepository, RewardsRepository, TagRepository, WalletSource, WalletType, WalletsRepository, }; use streamer::{ExchangeKind, ExchangeName, QueueName, StreamProducer, StreamProducerConfig}; pub async fn run_setup(settings: Settings) -> Result<(), Box> { info_with_fields!("setup", step = "init"); - let postgres_url = settings.postgres.url.as_str(); - let database: Database = Database::new(postgres_url, settings.postgres.pool); + let database: Database = Database::new(&settings.postgres.url, settings.postgres.pool); + + setup_database(&database)?; + setup_search_index(&settings).await?; + setup_queues(&settings).await?; + + info_with_fields!("setup", step = "complete"); + Ok(()) +} + +fn setup_database(database: &Database) -> Result<(), Box> { database.migrations()?.run_migrations().unwrap(); info_with_fields!("setup", step = "postgres migrations complete"); let chains = Chain::all(); - info_with_fields!("setup", step = "chains", chains = format!("{:?}", chains)); info_with_fields!("setup", step = "add chains"); @@ -46,7 +54,6 @@ pub async fn run_setup(settings: Settings) -> Result<(), Box Result<(), Box>(); - let _ = database.releases()?.add_releases(releases); info_with_fields!("setup", step = "assets tags"); @@ -74,6 +80,10 @@ pub async fn run_setup(settings: Settings) -> Result<(), Box>(); let _ = database.client()?.add_config(configs); + Ok(()) +} + +async fn setup_search_index(settings: &Settings) -> Result<(), Box> { info_with_fields!( "setup", step = "search index", @@ -83,6 +93,10 @@ pub async fn run_setup(settings: Settings) -> Result<(), Box Result<(), Box> { info_with_fields!("setup", step = "queues"); let chain_queues = QueueName::chain_queues(); @@ -90,20 +104,23 @@ pub async fn run_setup(settings: Settings) -> Result<(), Box>()), chains = format!("{:?}", chains) ); + for queue in &chain_queues { let exchange_name = format!("{}_exchange", queue); let _ = stream_producer.declare_exchange(&exchange_name, ExchangeKind::Topic).await; - for chain in &chains { + for chain in queue_supported_chains(queue, &chains) { let _ = stream_producer.bind_queue_routing_key(queue.clone(), chain.as_ref()).await; } } @@ -120,23 +137,37 @@ pub async fn run_setup(settings: Settings) -> Result<(), Box>()) ); for queue in &exchange_queues { - for chain in &chains { + for chain in queue_supported_chains(queue, &chains) { let queue_name = format!("{}.{}", queue, chain.as_ref()); let _ = stream_producer.bind_queue(&queue_name, &exchange.to_string(), chain.as_ref()).await; } } } - info_with_fields!("setup", step = "complete"); Ok(()) } +fn queue_supported_chains(queue: &QueueName, all_chains: &[Chain]) -> Vec { + match queue { + QueueName::FetchNftAssociations => NFTChain::all().into_iter().map(Into::into).collect(), + _ => all_chains.to_vec(), + } +} + pub async fn run_setup_dev(settings: Settings) -> Result<(), Box> { info_with_fields!("setup_dev", step = "init"); - let postgres_url = settings.postgres.url.as_str(); - let database: Database = Database::new(postgres_url, settings.postgres.pool); + let database: Database = Database::new(&settings.postgres.url, settings.postgres.pool); + setup_dev_currency(&database)?; + setup_dev_devices(&database)?; + setup_dev_assets(&database)?; + + info_with_fields!("setup_dev", step = "complete"); + Ok(()) +} + +fn setup_dev_currency(database: &Database) -> Result<(), Box> { info_with_fields!("setup_dev", step = "add currency"); let fiat_rate = FiatRateRow { @@ -146,8 +177,12 @@ pub async fn run_setup_dev(settings: Settings) -> Result<(), Box Result<(), Box> { info_with_fields!("setup_dev", step = "add devices"); let ios_device_id = "0".repeat(64); @@ -159,7 +194,7 @@ pub async fn run_setup_dev(settings: Settings) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box>(); - let _ = database.assets()?.add_assets(assets); - info_with_fields!("setup_dev", step = "add price alerts"); - let price_alerts = vec![ PriceAlert::new_price(AssetId::from_chain(Chain::Ethereum), "USD".to_string(), 3000.0, PriceAlertDirection::Up), PriceAlert::new_price(AssetId::from_chain(Chain::Bitcoin), "USD".to_string(), 50000.0, PriceAlertDirection::Down), ]; - let result = database.price_alerts()?.add_price_alerts(&ios_device_id, price_alerts)?; info_with_fields!("setup_dev", step = "price alerts added", count = result); + Ok(()) +} + +fn setup_dev_assets(database: &Database) -> Result<(), Box> { + info_with_fields!("setup_dev", step = "add assets"); + + let assets = Chain::all().into_iter().map(|x| Asset::from_chain(x).as_basic_primitive()).collect::>(); + let _ = database.assets()?.add_assets(assets); + info_with_fields!("setup_dev", step = "add fiat assets"); let ethereum_asset_id = AssetId::from_chain(Chain::Ethereum).to_string(); @@ -341,6 +361,5 @@ pub async fn run_setup_dev(settings: Settings) -> Result<(), Box (ShutdownSender, ShutdownReceiver) { pub fn spawn_signal_handler(shutdown_tx: ShutdownSender) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { - wait_for_signal().await; - info_with_fields!("shutdown signal received", status = "ok"); + let signal = wait_for_signal().await; + info_with_fields!("shutdown signal received", signal = signal, status = "ok"); let _ = shutdown_tx.send(true); }) } @@ -26,7 +26,7 @@ pub async fn wait_with_timeout(handles: Vec>, timeou tokio::time::timeout(timeout, futures::future::join_all(handles)).await.is_ok() } -async fn wait_for_signal() { +async fn wait_for_signal() -> &'static str { let ctrl_c = tokio::signal::ctrl_c(); #[cfg(unix)] @@ -43,7 +43,7 @@ async fn wait_for_signal() { let terminate = std::future::pending::<()>(); tokio::select! { - _ = ctrl_c => info_with_fields!("received SIGINT", status = "ok"), - _ = terminate => info_with_fields!("received SIGTERM", status = "ok"), + _ = ctrl_c => "SIGINT", + _ = terminate => "SIGTERM", } } diff --git a/apps/daemon/src/worker/alerter/mod.rs b/apps/daemon/src/worker/alerter/mod.rs index 4ef87ebed..49da10c9b 100644 --- a/apps/daemon/src/worker/alerter/mod.rs +++ b/apps/daemon/src/worker/alerter/mod.rs @@ -16,7 +16,8 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Result, Box> { let runtime = ctx.runtime(); @@ -83,24 +88,19 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Result Result, Box> { let runtime = ctx.runtime(); @@ -23,44 +22,34 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Self { - Self { cacher: cacher.clone() } - } - - fn key(job_name: &str) -> String { - format!("jobs:last_success:{}", job_name) - } - - fn boxed(error: E) -> JobError - where - E: std::error::Error + Send + Sync + 'static, - { - Box::new(error) - } - - async fn last_success(&self, job_name: &str) -> Result, JobError> { - let key = Self::key(job_name); - match self.cacher.get_value::(&key).await { - Ok(secs) => Ok(Some(UNIX_EPOCH + Duration::from_secs(secs))), - Err(err) => { - if err.downcast_ref::().is_some() { - Ok(None) - } else { - Err(err) - } - } - } - } -} - -#[async_trait] -impl JobSchedule for CacheJobSchedule { - async fn evaluate(&self, job_name: &str, interval: Duration, now: SystemTime) -> Result { - if let Some(last_success) = self.last_success(job_name).await? { - let elapsed = now.duration_since(last_success).unwrap_or_default(); - if elapsed < interval { - return Ok(RunDecision::Wait(interval - elapsed)); - } - } - Ok(RunDecision::Run) - } - - async fn mark_success(&self, job_name: &str, timestamp: SystemTime) -> Result<(), JobError> { - let duration = timestamp.duration_since(UNIX_EPOCH).map_err(Self::boxed)?; - self.cacher.set_value(&Self::key(job_name), &duration.as_secs()).await?; - Ok(()) - } -} diff --git a/apps/daemon/src/worker/job_reporter.rs b/apps/daemon/src/worker/job_reporter.rs deleted file mode 100644 index 314ed8003..000000000 --- a/apps/daemon/src/worker/job_reporter.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::future::Future; -use std::pin::Pin; - -use cacher::{CacheKey, CacherClient}; -use gem_tracing::info_with_fields; -use job_runner::JobStatusReporter; -use primitives::JobStatus; - -pub struct CacherJobReporter { - cacher: CacherClient, - service: String, -} - -impl CacherJobReporter { - pub fn new(cacher: CacherClient, service: &str) -> Self { - Self { - cacher, - service: service.to_string(), - } - } -} - -impl JobStatusReporter for CacherJobReporter { - fn report(&self, name: &str, interval: u64, duration: u64, success: bool, error: Option) -> Pin + Send + '_>> { - let normalized = format!("{}:{}", self.service, name); - Box::pin(async move { - let cache_key = CacheKey::JobStatus(&normalized); - let key = cache_key.key(); - let mut status = self.cacher.get_value::(&key).await.unwrap_or_default(); - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); - - status.interval = interval; - status.duration = duration; - - if success { - status.last_success = Some(timestamp); - status.last_error = None; - status.last_error_at = None; - } else if let Some(msg) = error { - status.last_error = Some(msg); - status.last_error_at = Some(timestamp); - } - - if let Err(e) = self.cacher.set_cached(cache_key, &status).await { - info_with_fields!("job status report failed", job = key, error = format!("{:?}", e)); - } - }) - } -} diff --git a/apps/daemon/src/worker/job_schedule.rs b/apps/daemon/src/worker/job_schedule.rs new file mode 100644 index 000000000..fe990d1a4 --- /dev/null +++ b/apps/daemon/src/worker/job_schedule.rs @@ -0,0 +1,80 @@ +use async_trait::async_trait; +use cacher::{CacheKey, CacherClient}; +use job_runner::{JobError, JobSchedule, JobStatusReporter, RunDecision}; +use primitives::JobStatus; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct CacherJobTracker { + cacher: CacherClient, + service: String, +} + +impl CacherJobTracker { + pub fn new(cacher: CacherClient, service: &str) -> Self { + Self { + cacher, + service: service.to_string(), + } + } + + fn job_key(&self, job_name: &str) -> String { + format!("{}:{}", self.service, job_name) + } + + async fn load_status(&self, job_name: &str) -> Option { + let cache_key = CacheKey::JobStatus(&self.job_key(job_name)); + self.cacher.get_value(&cache_key.key()).await.ok() + } + + async fn persist_status(&self, job_name: &str, status: &JobStatus) -> Result<(), JobError> { + let cache_key = CacheKey::JobStatus(&self.job_key(job_name)); + self.cacher.set_cached(cache_key, status).await + } +} + +#[async_trait] +impl JobStatusReporter for CacherJobTracker { + async fn report(&self, name: &str, interval: u64, duration: u64, success: bool, error: Option) { + let Some(mut status) = self.load_status(name).await else { + return; + }; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + + status.interval = interval; + status.duration = duration; + + if success { + status.last_success = Some(timestamp); + } else if let Some(msg) = error { + status.last_error = Some(msg); + status.last_error_at = Some(timestamp); + status.error_count += 1; + } + + let _ = self.persist_status(name, &status).await; + } +} + +#[async_trait] +impl JobSchedule for CacherJobTracker { + async fn evaluate(&self, job_name: &str, interval: Duration, now: SystemTime) -> Result { + let Some(status) = self.load_status(job_name).await else { + return Ok(RunDecision::Run); + }; + if let Some(last_success) = status.last_success { + let last_success_time = UNIX_EPOCH + Duration::from_secs(last_success); + let elapsed = now.duration_since(last_success_time).unwrap_or_default(); + if elapsed < interval { + return Ok(RunDecision::Wait(interval - elapsed)); + } + } + Ok(RunDecision::Run) + } + + async fn mark_success(&self, job_name: &str, timestamp: SystemTime) -> Result<(), JobError> { + let mut status = self.load_status(job_name).await.unwrap_or_default(); + let seconds = timestamp.duration_since(UNIX_EPOCH).map_err(|err| Box::new(err) as JobError)?.as_secs(); + status.last_success = Some(seconds); + self.persist_status(job_name, &status).await + } +} diff --git a/apps/daemon/src/worker/jobs.rs b/apps/daemon/src/worker/jobs.rs index 378b92406..1d561d0fb 100644 --- a/apps/daemon/src/worker/jobs.rs +++ b/apps/daemon/src/worker/jobs.rs @@ -1,5 +1,5 @@ use crate::model::WorkerService; -use primitives::ConfigKey; +use primitives::{Chain, ConfigKey, FiatProviderName, PlatformStore}; use std::error::Error; use std::time::Duration; use storage::ConfigCacher; @@ -35,6 +35,56 @@ impl JobSpec { } } +pub trait JobLabel { + fn job_label(&self) -> String; +} + +impl JobLabel for str { + fn job_label(&self) -> String { + self.to_string() + } +} + +impl JobLabel for String { + fn job_label(&self) -> String { + self.clone() + } +} + +impl JobLabel for &T +where + T: JobLabel + ?Sized, +{ + fn job_label(&self) -> String { + (*self).job_label() + } +} + +impl JobLabel for Chain { + fn job_label(&self) -> String { + self.as_ref().to_string() + } +} + +impl JobLabel for FiatProviderName { + fn job_label(&self) -> String { + self.as_ref().to_string() + } +} + +impl JobLabel for PlatformStore { + fn job_label(&self) -> String { + self.as_ref().to_string() + } +} + +fn compose_job_name(base: &str, label: Option<&str>) -> String { + match label.map(str::trim).filter(|value| !value.is_empty()) { + Some(suffix) => format!("{base}.{suffix}"), + None => base.to_string(), + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum WorkerJob { @@ -60,7 +110,7 @@ pub enum WorkerJob { UpdatePerpetualsIndex, UpdateNftsIndex, CleanupProcessedTransactions, - UpdateStoreVersions, + UpdateStoreVersion, UpdateChainValidators, UpdateValidatorsFromStaticAssets, CheckRewardsAbuse, @@ -69,9 +119,11 @@ pub enum WorkerJob { UpdatePricesTopMarketCap, UpdatePricesHighMarketCap, UpdatePricesLowMarketCap, + UpdatePricesVeryLowMarketCap, AggregateHourlyCharts, AggregateDailyCharts, - CleanupChartsData, + CleanupChartsHourly, + CleanupChartsDaily, UpdateMarkets, UpdateObservedPrices, UpdateDexFeeds, @@ -93,33 +145,35 @@ impl WorkerJob { UpdatePerpetuals => JobSpec::new(WorkerService::Assets, JobInterval::Config(ConfigKey::AssetsTimerUpdatePerpetuals)), UpdateUsageRanks => JobSpec::new(WorkerService::Assets, JobInterval::Config(ConfigKey::AssetsTimerUpdateUsageRank)), UpdateAssetsImages => JobSpec::new(WorkerService::Assets, JobInterval::Config(ConfigKey::AssetsTimerUpdateImages)), - CleanupStaleDeviceSubscriptions => JobSpec::new(WorkerService::Device, JobInterval::Config(ConfigKey::DeviceTimerUpdater)), - ObserveInactiveDevices => JobSpec::new(WorkerService::Device, JobInterval::Config(ConfigKey::DeviceTimerInactiveObserver)), + CleanupStaleDeviceSubscriptions => JobSpec::new(WorkerService::System, JobInterval::Config(ConfigKey::DeviceTimerUpdater)), + ObserveInactiveDevices => JobSpec::new(WorkerService::System, JobInterval::Config(ConfigKey::DeviceTimerInactiveObserver)), UpdateFiatAssets => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateAssets)), UpdateFiatProviderCountries => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateProviderCountries)), UpdateFiatBuyableAssets => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateBuyableAssets)), - UpdateFiatSellableAssets => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateBuyableAssets)), + UpdateFiatSellableAssets => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateSellableAssets)), UpdateTrendingFiatAssets => JobSpec::new(WorkerService::Fiat, JobInterval::Config(ConfigKey::FiatTimerUpdateTrending)), UpdateAssetsIndex => JobSpec::new(WorkerService::Search, JobInterval::Config(ConfigKey::SearchAssetsUpdateInterval)), UpdatePerpetualsIndex => JobSpec::new(WorkerService::Search, JobInterval::Config(ConfigKey::SearchPerpetualsUpdateInterval)), UpdateNftsIndex => JobSpec::new(WorkerService::Search, JobInterval::Config(ConfigKey::SearchNftsUpdateInterval)), - CleanupProcessedTransactions => JobSpec::new(WorkerService::Transaction, JobInterval::Config(ConfigKey::TransactionTimerUpdater)), - UpdateStoreVersions => JobSpec::new(WorkerService::Version, JobInterval::Config(ConfigKey::VersionTimerUpdateStoreVersions)), - UpdateChainValidators => JobSpec::new(WorkerService::Scan, JobInterval::Config(ConfigKey::ScanTimerUpdateValidators)), - UpdateValidatorsFromStaticAssets => JobSpec::new(WorkerService::Scan, JobInterval::Config(ConfigKey::ScanTimerUpdateValidatorsStatic)), + CleanupProcessedTransactions => JobSpec::new(WorkerService::System, JobInterval::Config(ConfigKey::TransactionTimerUpdater)), + UpdateStoreVersion => JobSpec::new(WorkerService::System, JobInterval::Config(ConfigKey::VersionTimerUpdateStoreVersions)), + UpdateChainValidators => JobSpec::new(WorkerService::Assets, JobInterval::Config(ConfigKey::ScanTimerUpdateValidators)), + UpdateValidatorsFromStaticAssets => JobSpec::new(WorkerService::Assets, JobInterval::Config(ConfigKey::ScanTimerUpdateValidatorsStatic)), CheckRewardsAbuse => JobSpec::new(WorkerService::Rewards, JobInterval::Config(ConfigKey::RewardsTimerAbuseChecker)), - CleanupOutdatedAssets => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerCleanOutdated)), - UpdateFiatRates => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerFiatRates)), - UpdatePricesTopMarketCap => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerTopMarketCap)), - UpdatePricesHighMarketCap => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerHighMarketCap)), - UpdatePricesLowMarketCap => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerLowMarketCap)), - AggregateHourlyCharts => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerChartsHourly)), - AggregateDailyCharts => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerChartsDaily)), - CleanupChartsData => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerCleanupCharts)), - UpdateMarkets => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceTimerMarkets)), - UpdateObservedPrices => JobSpec::new(WorkerService::Pricer, JobInterval::Config(ConfigKey::PriceObservedFetchInterval)), - UpdateDexFeeds => JobSpec::new(WorkerService::PricesDex, JobInterval::Duration(Duration::from_secs(3600))), - UpdateDexPrices => JobSpec::new(WorkerService::PricesDex, JobInterval::Duration(Duration::from_secs(1800))), + CleanupOutdatedAssets => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerCleanOutdated)), + UpdateFiatRates => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerFiatRates)), + UpdatePricesTopMarketCap => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerTopMarketCap)), + UpdatePricesHighMarketCap => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerHighMarketCap)), + UpdatePricesLowMarketCap => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerLowMarketCap)), + UpdatePricesVeryLowMarketCap => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerVeryLowMarketCap)), + AggregateHourlyCharts => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerChartsHourly)), + AggregateDailyCharts => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerChartsDaily)), + CleanupChartsHourly => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerCleanupChartsHourly)), + CleanupChartsDaily => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerCleanupChartsDaily)), + UpdateMarkets => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceTimerMarkets)), + UpdateObservedPrices => JobSpec::new(WorkerService::Prices, JobInterval::Config(ConfigKey::PriceObservedFetchInterval)), + UpdateDexFeeds => JobSpec::new(WorkerService::Prices, JobInterval::Duration(Duration::from_secs(3600))), + UpdateDexPrices => JobSpec::new(WorkerService::Prices, JobInterval::Duration(Duration::from_secs(1800))), } } @@ -133,32 +187,25 @@ impl WorkerJob { } #[derive(Clone, Debug)] -pub struct JobInstance { +pub struct JobVariant { job: WorkerJob, - name: String, + label: Option, override_interval: Option, } -impl JobInstance { +impl JobVariant { pub fn new(job: WorkerJob) -> Self { Self { - name: job.as_ref().to_string(), job, + label: None, override_interval: None, } } pub fn labeled(job: WorkerJob, label: impl Into) -> Self { - let label = label.into(); - let trimmed = label.trim(); - let name = if trimmed.is_empty() { - job.as_ref().to_string() - } else { - format!("{}.{}", job.as_ref(), trimmed) - }; Self { job, - name, + label: Some(label.into()), override_interval: None, } } @@ -168,8 +215,8 @@ impl JobInstance { self } - pub fn name(&self) -> &str { - &self.name + pub fn name(&self) -> String { + job_name(self.job, self.label.as_deref()) } pub fn worker(&self) -> WorkerService { @@ -185,8 +232,12 @@ impl JobInstance { } } -impl From for JobInstance { +impl From for JobVariant { fn from(job: WorkerJob) -> Self { - JobInstance::new(job) + JobVariant::new(job) } } + +pub fn job_name(job: WorkerJob, label: Option<&str>) -> String { + compose_job_name(job.as_ref(), label) +} diff --git a/apps/daemon/src/worker/mod.rs b/apps/daemon/src/worker/mod.rs index cbc3da1bd..a1db2028b 100644 --- a/apps/daemon/src/worker/mod.rs +++ b/apps/daemon/src/worker/mod.rs @@ -1,17 +1,32 @@ pub mod alerter; pub mod assets; pub mod context; -pub mod device; pub mod fiat; -pub mod job_history; -pub mod job_reporter; +pub mod job_schedule; pub mod jobs; pub mod plan; -pub mod pricer; -pub mod prices_dex; +pub mod prices; pub mod rewards; pub mod runtime; -pub mod scan; pub mod search; -pub mod transaction; -pub mod version; +pub mod system; + +use crate::model::WorkerService; +use crate::shutdown::ShutdownReceiver; +use crate::worker::context::WorkerContext; +use job_runner::JobHandle; +use std::error::Error; + +impl WorkerService { + pub async fn run_jobs(self, ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { + match self { + WorkerService::Alerter => alerter::jobs(ctx, shutdown_rx).await, + WorkerService::Prices => prices::jobs(ctx, shutdown_rx).await, + WorkerService::Fiat => fiat::jobs(ctx, shutdown_rx).await, + WorkerService::Assets => assets::jobs(ctx, shutdown_rx).await, + WorkerService::System => system::jobs(ctx, shutdown_rx).await, + WorkerService::Search => search::jobs(ctx, shutdown_rx).await, + WorkerService::Rewards => rewards::jobs(ctx, shutdown_rx).await, + } + } +} diff --git a/apps/daemon/src/worker/plan.rs b/apps/daemon/src/worker/plan.rs index 8ab8a6c4e..1cc3c355c 100644 --- a/apps/daemon/src/worker/plan.rs +++ b/apps/daemon/src/worker/plan.rs @@ -1,5 +1,5 @@ use crate::model::WorkerService; -use crate::worker::jobs::{JobInstance, WorkerJob}; +use crate::worker::jobs::{JobLabel, JobVariant, WorkerJob}; use job_runner::{JobError, JobHandle, JobPlan}; use std::error::Error; use std::fmt::Debug; @@ -15,14 +15,6 @@ pub struct JobPlanBuilder<'a> { } impl<'a> JobPlanBuilder<'a> { - pub fn new(worker: WorkerService, plan: JobPlan) -> Self { - Self { - worker, - plan: Ok(plan), - config: None, - } - } - pub fn with_config(worker: WorkerService, plan: JobPlan, config: &'a ConfigCacher) -> Self { Self { worker, @@ -33,19 +25,19 @@ impl<'a> JobPlanBuilder<'a> { pub fn job(self, job: J, job_fn: F) -> Self where - J: Into, + J: Into, F: Fn() -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, R: Debug + Send + Sync + 'static, { let config = self.config; let plan = self.plan.and_then(|plan| { - let instance: JobInstance = job.into(); - if instance.worker() != self.worker { - return Err(format!("job {} belongs to {:?} worker but builder is {:?}", instance.name(), instance.worker(), self.worker).into()); + let variant: JobVariant = job.into(); + if variant.worker() != self.worker { + return Err(format!("job {} belongs to {:?} worker but builder is {:?}", variant.name(), variant.worker(), self.worker).into()); } - let interval = instance.resolve_interval(config)?; - Ok(plan.job(instance.name().to_string(), interval, job_fn)) + let interval = variant.resolve_interval(config)?; + Ok(plan.job(variant.name(), interval, job_fn)) }); Self { worker: self.worker, @@ -54,12 +46,11 @@ impl<'a> JobPlanBuilder<'a> { } } - pub fn jobs(self, job: WorkerJob, items: T, labeler: Labeler, build_job: Build) -> Self + pub fn jobs(self, job: WorkerJob, items: Items, build_job: Builder) -> Self where - T: IntoIterator, - Item: Clone + Send + Sync + 'static, - Labeler: Fn(&Item) -> String, - Build: Fn(Item, JobInstance) -> F, + Items: IntoIterator, + Item: JobLabel + Clone + Send + Sync + 'static, + Builder: Fn(Item, JobVariant) -> F, F: Fn() -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, R: Debug + Send + Sync + 'static, @@ -70,11 +61,10 @@ impl<'a> JobPlanBuilder<'a> { return Err(format!("job {} belongs to {:?} worker but builder is {:?}", job.as_ref(), job.worker(), self.worker).into()); } items.into_iter().try_fold(plan, |current, item| { - let label = labeler(&item); - let instance = JobInstance::labeled(job, label); - let interval = instance.resolve_interval(config)?; - let job_fn = build_job(item.clone(), instance.clone()); - Ok(current.job(instance.name().to_string(), interval, job_fn)) + let variant = JobVariant::labeled(job, item.job_label()); + let interval = variant.resolve_interval(config)?; + let job_fn = build_job(item.clone(), variant.clone()); + Ok(current.job(variant.name(), interval, job_fn)) }) }); Self { diff --git a/apps/daemon/src/worker/pricer/charts_updater.rs b/apps/daemon/src/worker/prices/charts_updater.rs similarity index 81% rename from apps/daemon/src/worker/pricer/charts_updater.rs rename to apps/daemon/src/worker/prices/charts_updater.rs index d35e3091a..d19628bb4 100644 --- a/apps/daemon/src/worker/pricer/charts_updater.rs +++ b/apps/daemon/src/worker/prices/charts_updater.rs @@ -1,5 +1,6 @@ use coingecko::CoinGeckoClient; use pricer::PriceClient; +use primitives::ChartTimeframe; use std::error::Error; use storage::models::ChartRow; use streamer::{ChartsPayload, StreamProducer, StreamProducerQueue}; @@ -54,15 +55,11 @@ impl ChartsUpdater { Ok(coin_list.len()) } - pub async fn aggregate_hourly_charts(&self) -> Result> { - self.prices_client.aggregate_hourly_charts().await + pub async fn aggregate_charts(&self, timeframe: ChartTimeframe) -> Result> { + self.prices_client.aggregate_charts(timeframe).await } - pub async fn aggregate_daily_charts(&self) -> Result> { - self.prices_client.aggregate_daily_charts().await - } - - pub async fn cleanup_charts_data(&self) -> Result> { - self.prices_client.cleanup_charts_data().await + pub async fn cleanup_charts(&self, timeframe: ChartTimeframe) -> Result> { + self.prices_client.cleanup_charts(timeframe).await } } diff --git a/apps/daemon/src/worker/pricer/markets_updater.rs b/apps/daemon/src/worker/prices/markets_updater.rs similarity index 100% rename from apps/daemon/src/worker/pricer/markets_updater.rs rename to apps/daemon/src/worker/prices/markets_updater.rs diff --git a/apps/daemon/src/worker/pricer/mod.rs b/apps/daemon/src/worker/prices/mod.rs similarity index 68% rename from apps/daemon/src/worker/pricer/mod.rs rename to apps/daemon/src/worker/prices/mod.rs index c6534facd..14f81b06a 100644 --- a/apps/daemon/src/worker/pricer/mod.rs +++ b/apps/daemon/src/worker/prices/mod.rs @@ -2,13 +2,15 @@ mod charts_updater; mod markets_updater; mod observed_prices_updater; pub mod price_updater; +mod prices_dex_updater; use crate::model::WorkerService; use crate::worker::context::WorkerContext; -use crate::worker::jobs::WorkerJob; +use crate::worker::jobs::{JobVariant, WorkerJob}; use crate::worker::plan::JobPlanBuilder; use std::error::Error; use std::sync::Arc; +use std::time::Duration; use cacher::CacherClient; use charts_updater::ChartsUpdater; @@ -18,11 +20,20 @@ use markets_updater::MarketsUpdater; use observed_prices_updater::ObservedPricesUpdater; use price_updater::{PriceUpdater, UpdatePrices}; use pricer::{MarketsClient, PriceClient}; -use primitives::ConfigKey; +use prices_dex::PriceFeedProvider; +use prices_dex_updater::PricesDexUpdater; +use primitives::{ChartTimeframe, ConfigKey}; use settings::Settings; use storage::{ConfigCacher, Database}; use streamer::{StreamProducer, StreamProducerConfig}; +struct DexProviderConfig { + provider_type: PriceFeedProvider, + name: &'static str, + url: String, + timer: u64, +} + pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { let runtime = ctx.runtime(); let database = ctx.database(); @@ -31,7 +42,22 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Result Result Result Result Result Result Result> { let coingecko_client = CoinGeckoClient::new(&settings.coingecko.key.secret.clone()); let price_client = PriceClient::new(database.clone(), cacher.clone()); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); let stream_producer = StreamProducer::new(&rabbitmq_config, "pricer_worker").await?; Ok(PriceUpdater::new(price_client, coingecko_client, stream_producer)) } @@ -214,7 +305,8 @@ async fn charts_updater_factory( coingecko_client: CoinGeckoClient, ) -> Result> { let price_client = PriceClient::new(database.clone(), cacher.clone()); - let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), settings.rabbitmq.retry_delay, settings.rabbitmq.retry_max_delay); + let retry = streamer::Retry::new(settings.rabbitmq.retry.delay, settings.rabbitmq.retry.timeout); + let rabbitmq_config = StreamProducerConfig::new(settings.rabbitmq.url.clone(), retry); let stream_producer = StreamProducer::new(&rabbitmq_config, "charts_worker").await?; Ok(ChartsUpdater::new(price_client, coingecko_client, stream_producer)) } diff --git a/apps/daemon/src/worker/pricer/observed_prices_updater.rs b/apps/daemon/src/worker/prices/observed_prices_updater.rs similarity index 100% rename from apps/daemon/src/worker/pricer/observed_prices_updater.rs rename to apps/daemon/src/worker/prices/observed_prices_updater.rs diff --git a/apps/daemon/src/worker/pricer/price_updater.rs b/apps/daemon/src/worker/prices/price_updater.rs similarity index 95% rename from apps/daemon/src/worker/pricer/price_updater.rs rename to apps/daemon/src/worker/prices/price_updater.rs index 877a77a25..9782a1bbf 100644 --- a/apps/daemon/src/worker/pricer/price_updater.rs +++ b/apps/daemon/src/worker/prices/price_updater.rs @@ -16,6 +16,7 @@ pub enum UpdatePrices { Top, High, Low, + VeryLow, } const MAX_MARKETS_PER_PAGE: usize = 250; @@ -34,7 +35,8 @@ impl PriceUpdater { let asset_ids = match update_type { UpdatePrices::Top => ids.into_iter().take(500).collect::>(), UpdatePrices::High => ids.into_iter().take(2500).skip(500).collect::>(), - UpdatePrices::Low => ids.into_iter().skip(2500).collect::>(), + UpdatePrices::Low => ids.into_iter().take(5000).skip(2500).collect::>(), + UpdatePrices::VeryLow => ids.into_iter().skip(5000).collect::>(), }; self.update_prices(asset_ids).await } diff --git a/apps/daemon/src/worker/prices_dex/prices_dex_updater.rs b/apps/daemon/src/worker/prices/prices_dex_updater.rs similarity index 100% rename from apps/daemon/src/worker/prices_dex/prices_dex_updater.rs rename to apps/daemon/src/worker/prices/prices_dex_updater.rs diff --git a/apps/daemon/src/worker/prices_dex/mod.rs b/apps/daemon/src/worker/prices_dex/mod.rs deleted file mode 100644 index e76f7b9cc..000000000 --- a/apps/daemon/src/worker/prices_dex/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -pub mod prices_dex_updater; - -use crate::model::WorkerService; -use crate::worker::context::WorkerContext; -use crate::worker::jobs::{JobInstance, WorkerJob}; -use crate::worker::plan::JobPlanBuilder; -use job_runner::{JobHandle, ShutdownReceiver}; -use prices_dex::PriceFeedProvider; -pub use prices_dex_updater::PricesDexUpdater; -use std::time::Duration; - -struct ProviderConfig { - provider_type: PriceFeedProvider, - name: &'static str, - url: String, - timer: u64, -} - -pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { - let runtime = ctx.runtime(); - let database = ctx.database(); - let settings = ctx.settings(); - let providers = vec![ - ProviderConfig { - provider_type: PriceFeedProvider::Pyth, - name: "Pyth", - url: settings.prices.pyth.url.clone(), - timer: settings.prices.pyth.timer, - }, - ProviderConfig { - provider_type: PriceFeedProvider::Jupiter, - name: "Jupiter", - url: settings.prices.jupiter.url.clone(), - timer: settings.prices.jupiter.timer, - }, - ]; - - providers - .into_iter() - .fold(JobPlanBuilder::new(WorkerService::PricesDex, runtime.plan(shutdown_rx)), |builder, provider| { - let slug = provider.name.to_lowercase(); - let builder = builder.job(JobInstance::labeled(WorkerJob::UpdateDexFeeds, slug.clone()).every(Duration::from_secs(3600)), { - let url = provider.url.clone(); - let database = database.clone(); - let provider_type = provider.provider_type.clone(); - move || { - let url = url.clone(); - let database = database.clone(); - let provider_type = provider_type.clone(); - async move { PricesDexUpdater::new(provider_type, &url, database).update_feeds().await } - } - }); - - builder.job(JobInstance::labeled(WorkerJob::UpdateDexPrices, slug).every(Duration::from_secs(provider.timer)), { - let url = provider.url.clone(); - let database = database.clone(); - let provider_type = provider.provider_type.clone(); - move || { - let url = url.clone(); - let database = database.clone(); - let provider_type = provider_type.clone(); - async move { PricesDexUpdater::new(provider_type, &url, database).update_prices().await } - } - }) - }) - .finish() -} diff --git a/apps/daemon/src/worker/rewards/mod.rs b/apps/daemon/src/worker/rewards/mod.rs index 3fc23fcf3..a0e8c5365 100644 --- a/apps/daemon/src/worker/rewards/mod.rs +++ b/apps/daemon/src/worker/rewards/mod.rs @@ -15,7 +15,8 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Result, Box> { - let runtime = ctx.runtime(); - let database = ctx.database(); - let settings = ctx.settings(); - let config = ConfigCacher::new(database.clone()); - let assets_url = Arc::new(settings.assets.url.clone()); - - let validator_providers = Arc::new(ChainProviders::from_settings(&settings, &service_user_agent("daemon", Some("scan_validators")))); - let static_providers = Arc::new(ChainProviders::from_settings(&settings, &service_user_agent("daemon", Some("scan_static_assets")))); - - JobPlanBuilder::with_config(WorkerService::Scan, runtime.plan(shutdown_rx), &config) - .jobs( - WorkerJob::UpdateChainValidators, - Chain::stakeable(), - |chain| chain.as_ref().to_string(), - |chain, _| { - let providers = validator_providers.clone(); - let database = database.clone(); - move || { - let providers = providers.clone(); - let database = database.clone(); - async move { - let scanner = ValidatorScanner::new(providers, database); - scanner.update_validators_for_chain(chain).await - } - } - }, - ) - .jobs( - WorkerJob::UpdateValidatorsFromStaticAssets, - [Chain::Tron, Chain::SmartChain], - |chain| chain.as_ref().to_string(), - |chain, _| { - let providers = static_providers.clone(); - let database = database.clone(); - let assets_url = assets_url.clone(); - move || { - let providers = providers.clone(); - let database = database.clone(); - let assets_url = assets_url.clone(); - async move { - let scanner = ValidatorScanner::new(providers, database); - scanner.update_validators_from_static_assets_for_chain(chain, assets_url.as_str()).await - } - } - }, - ) - .finish() -} diff --git a/apps/daemon/src/worker/device/device_updater.rs b/apps/daemon/src/worker/system/device_updater.rs similarity index 100% rename from apps/daemon/src/worker/device/device_updater.rs rename to apps/daemon/src/worker/system/device_updater.rs diff --git a/apps/daemon/src/worker/device/mod.rs b/apps/daemon/src/worker/system/mod.rs similarity index 63% rename from apps/daemon/src/worker/device/mod.rs rename to apps/daemon/src/worker/system/mod.rs index 987deeab1..4e3880acd 100644 --- a/apps/daemon/src/worker/device/mod.rs +++ b/apps/daemon/src/worker/system/mod.rs @@ -1,5 +1,8 @@ mod device_updater; +mod model; mod observers; +mod transaction_updater; +mod version_updater; use crate::model::WorkerService; use crate::worker::context::WorkerContext; @@ -12,6 +15,8 @@ use observers::InactiveDevicesObserver; use std::error::Error; use storage::ConfigCacher; use streamer::{StreamProducer, StreamProducerConfig}; +use transaction_updater::TransactionUpdater; +use version_updater::VersionUpdater; pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { let runtime = ctx.runtime(); @@ -20,7 +25,15 @@ pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result Result Result>(); let result = self.database.transactions()?.delete_transactions_addresses(addresses.clone())?; diff --git a/apps/daemon/src/worker/system/version_updater.rs b/apps/daemon/src/worker/system/version_updater.rs new file mode 100644 index 000000000..0cb234df4 --- /dev/null +++ b/apps/daemon/src/worker/system/version_updater.rs @@ -0,0 +1,73 @@ +use primitives::{PlatformStore, config::Release}; +use std::error::Error; +use storage::{Database, ReleasesRepository}; + +use super::model::{GitHubRepository, ITunesLookupResponse, SamsungStoreDetail}; + +pub struct VersionUpdater { + database: Database, +} + +impl VersionUpdater { + pub fn new(database: Database) -> Self { + Self { database } + } + + pub fn stores() -> &'static [PlatformStore] { + &[PlatformStore::AppStore, PlatformStore::ApkUniversal, PlatformStore::SamsungStore] + } + + pub async fn update_store(&self, store: PlatformStore) -> Result> { + let version = self.get_store_version(store).await?; + let current = self.get_current_version(store)?; + + if current.as_ref() != Some(&version) { + self.set_release(Release::new(store, version.clone(), false))?; + } + + Ok(version) + } + + fn get_current_version(&self, store: PlatformStore) -> Result, Box> { + let releases = self.database.releases()?.get_releases()?; + let version = releases.into_iter().find(|r| r.platform_store.0 == store).map(|r| r.version); + Ok(version) + } + + async fn get_store_version(&self, store: PlatformStore) -> Result> { + match store { + PlatformStore::AppStore => self.get_app_store_version().await, + PlatformStore::ApkUniversal => self.get_github_version().await, + PlatformStore::SamsungStore => self.get_samsung_version().await, + _ => Err(format!("unsupported store: {:?}", store).into()), + } + } + + fn set_release(&self, release: Release) -> Result<(), Box> { + let row = storage::models::ReleaseRow::from_primitive(release); + self.database.releases()?.update_release(row)?; + Ok(()) + } + + async fn get_app_store_version(&self) -> Result> { + let url = "https://itunes.apple.com/lookup?bundleId=com.gemwallet.ios"; + let response = reqwest::get(url).await?.json::().await?; + response.results.first().map(|r| r.version.clone()).ok_or_else(|| "no results".into()) + } + + async fn get_github_version(&self) -> Result> { + let url = "https://api.github.com/repos/gemwalletcom/gem-android/releases"; + let response = reqwest::Client::new().get(url).send().await?.json::>().await?; + response + .into_iter() + .find(|x| !x.draft && !x.prerelease && x.assets.iter().any(|a| a.name.contains("gem_wallet_universal_"))) + .map(|r| r.name) + .ok_or_else(|| "no releases".into()) + } + + async fn get_samsung_version(&self) -> Result> { + let url = "https://galaxystore.samsung.com/api/detail/com.gemwallet.android"; + let response = reqwest::get(url).await?.json::().await?; + Ok(response.details.version) + } +} diff --git a/apps/daemon/src/worker/transaction/mod.rs b/apps/daemon/src/worker/transaction/mod.rs deleted file mode 100644 index 56033cf10..000000000 --- a/apps/daemon/src/worker/transaction/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod transaction_updater; - -use crate::model::WorkerService; -use crate::worker::context::WorkerContext; -use crate::worker::jobs::WorkerJob; -use crate::worker::plan::JobPlanBuilder; -use job_runner::{JobHandle, ShutdownReceiver}; -use std::error::Error; -use storage::ConfigCacher; -use transaction_updater::TransactionUpdater; - -pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { - let runtime = ctx.runtime(); - let database = ctx.database(); - let config = ConfigCacher::new(database.clone()); - - JobPlanBuilder::with_config(WorkerService::Transaction, runtime.plan(shutdown_rx), &config) - .job(WorkerJob::CleanupProcessedTransactions, { - let database = database.clone(); - move || { - let database = database.clone(); - let transaction_updater = TransactionUpdater::new(database); - async move { transaction_updater.update().await } - } - }) - .finish() -} diff --git a/apps/daemon/src/worker/version/mod.rs b/apps/daemon/src/worker/version/mod.rs deleted file mode 100644 index f348bdba0..000000000 --- a/apps/daemon/src/worker/version/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod model; -mod version_updater; - -use crate::model::WorkerService; -use crate::worker::context::WorkerContext; -use crate::worker::jobs::WorkerJob; -use crate::worker::plan::JobPlanBuilder; -use job_runner::{JobHandle, ShutdownReceiver}; -use std::error::Error; -use storage::ConfigCacher; -use version_updater::VersionClient; - -pub async fn jobs(ctx: WorkerContext, shutdown_rx: ShutdownReceiver) -> Result, Box> { - let runtime = ctx.runtime(); - let database = ctx.database(); - let config = ConfigCacher::new(database.clone()); - - JobPlanBuilder::with_config(WorkerService::Version, runtime.plan(shutdown_rx), &config) - .job(WorkerJob::UpdateStoreVersions, { - let database = database.clone(); - move || { - let database = database.clone(); - let version_client = VersionClient::new(database); - async move { version_client.update_store_versions().await } - } - }) - .finish() -} diff --git a/apps/daemon/src/worker/version/version_updater.rs b/apps/daemon/src/worker/version/version_updater.rs deleted file mode 100644 index 54c991d11..000000000 --- a/apps/daemon/src/worker/version/version_updater.rs +++ /dev/null @@ -1,86 +0,0 @@ -use gem_tracing::info_with_fields; -use primitives::{PlatformStore, config::Release}; -use std::error::Error; -use storage::{Database, ReleasesRepository}; - -use super::model::{GitHubRepository, ITunesLookupResponse, SamsungStoreDetail}; - -pub struct VersionClient { - database: Database, -} - -impl VersionClient { - pub fn new(database: Database) -> Self { - Self { database } - } - - pub async fn update_store_versions(&self) -> Result, Box> { - let platforms = [PlatformStore::AppStore, PlatformStore::ApkUniversal, PlatformStore::SamsungStore]; - let mut updates = Vec::new(); - - for platform in platforms { - let version = match platform { - PlatformStore::AppStore => self.update_app_store_version().await?, - PlatformStore::ApkUniversal => self.update_apk_version().await?, - PlatformStore::SamsungStore => self.update_samsung_store_version().await?, - _ => continue, - }; - - info_with_fields!("update_store_version", platform = platform.as_ref(), version = version.as_str()); - updates.push((platform.as_ref().to_string(), version)); - } - Ok(updates) - } - - async fn update_app_store_version(&self) -> Result> { - let version = self.get_app_store_version().await?; - self.set_release(Release::new(PlatformStore::AppStore, version.clone(), false))?; - Ok(version) - } - - async fn update_apk_version(&self) -> Result> { - let version = self.get_github_apk_version().await?; - self.set_release(Release::new(PlatformStore::ApkUniversal, version.clone(), false))?; - Ok(version) - } - - async fn update_samsung_store_version(&self) -> Result> { - let url = "https://galaxystore.samsung.com/api/detail/com.gemwallet.android"; - let response = reqwest::get(url).await?.json::().await?; - let version = response.details.version.clone(); - self.set_release(Release::new(PlatformStore::SamsungStore, version.clone(), false))?; - Ok(version) - } - - fn set_release(&self, release: Release) -> Result> { - let releases = storage::models::ReleaseRow::from_primitive(release.clone()).clone(); - let _ = self.database.releases()?.update_release(releases)?; - Ok(release) - } - - async fn get_app_store_version(&self) -> Result> { - let url = "https://itunes.apple.com/lookup?bundleId=com.gemwallet.ios"; - let response = reqwest::get(url).await?.json::().await?; - let version = response - .results - .first() - .map(|result| result.version.to_string()) - .ok_or_else(|| "app store lookup returned no results".to_string())?; - Ok(version) - } - - async fn get_github_apk_version(&self) -> Result> { - let url = "https://api.github.com/repos/gemwalletcom/gem-android/releases"; - let client = reqwest::Client::new(); - let response = client.get(url).send().await?.json::>().await?; - let results = response - .into_iter() - .filter(|x| !x.draft && !x.prerelease && x.assets.clone().into_iter().any(|x| x.name.contains("gem_wallet_universal_"))) - .collect::>(); - let version = results - .first() - .map(|result| result.name.clone()) - .ok_or_else(|| "github releases list is empty".to_string())?; - Ok(version) - } -} diff --git a/crates/cacher/src/keys.rs b/crates/cacher/src/keys.rs index 386ba2a27..35c2c0365 100644 --- a/crates/cacher/src/keys.rs +++ b/crates/cacher/src/keys.rs @@ -2,9 +2,6 @@ const SECONDS_PER_MINUTE: u64 = 60; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; pub enum CacheKey<'a> { - ParserCurrentBlock(&'a str), - ParserLatestBlock(&'a str), - // Referral keys ReferralIpCheck(&'a str), @@ -46,8 +43,6 @@ pub enum CacheKey<'a> { impl CacheKey<'_> { pub fn key(&self) -> String { match self { - Self::ParserCurrentBlock(chain) => format!("parser:state:{}:current_block", chain), - Self::ParserLatestBlock(chain) => format!("parser:state:{}:latest_block", chain), Self::ReferralIpCheck(ip_address) => format!("referral:ip_check:{}", ip_address), Self::UsernameCreationPerIp(ip_address) => format!("username:ip:{}", ip_address), Self::UsernameCreationPerDevice(device_id) => format!("username:device:{}", device_id), @@ -72,8 +67,6 @@ impl CacheKey<'_> { pub fn ttl(&self) -> u64 { match self { - Self::ParserCurrentBlock(_) => 7 * SECONDS_PER_DAY, - Self::ParserLatestBlock(_) => 7 * SECONDS_PER_DAY, Self::ReferralIpCheck(_) => 30 * SECONDS_PER_DAY, Self::UsernameCreationPerIp(_) => 30 * SECONDS_PER_DAY, Self::UsernameCreationPerDevice(_) => 30 * SECONDS_PER_DAY, diff --git a/crates/cacher/src/lib.rs b/crates/cacher/src/lib.rs index ed982f0f1..773b023d9 100644 --- a/crates/cacher/src/lib.rs +++ b/crates/cacher/src/lib.rs @@ -15,8 +15,8 @@ pub struct CacherClient { impl CacherClient { pub async fn new(redis_url: &str) -> Self { - let client = Client::open(redis_url).unwrap(); - let connection = ConnectionManager::new(client).await.unwrap(); + let client = Client::open(redis_url).expect("invalid redis url"); + let connection = ConnectionManager::new(client).await.expect("failed to connect to redis"); Self { connection } } @@ -219,4 +219,18 @@ impl CacherClient { .query_async(&mut self.connection.clone()) .await?) } + + pub async fn sorted_set_card(&self, key: &str) -> Result> { + Ok(redis::cmd("ZCARD").arg(key).query_async(&mut self.connection.clone()).await?) + } + + pub async fn sorted_set_rev_range_with_scores(&self, key: &str, start: isize, stop: isize) -> Result, Box> { + Ok(redis::cmd("ZREVRANGE") + .arg(key) + .arg(start) + .arg(stop) + .arg("WITHSCORES") + .query_async(&mut self.connection.clone()) + .await?) + } } diff --git a/crates/fiat/src/client.rs b/crates/fiat/src/client.rs index 8ebd81062..9b67b87ad 100644 --- a/crates/fiat/src/client.rs +++ b/crates/fiat/src/client.rs @@ -17,7 +17,7 @@ use primitives::{ }; use reqwest::Client as RequestClient; use storage::{ - AssetFilter, AssetsRepository, ConfigCacher, Database, SubscriptionsRepository, WalletsRepository, + AssetFilter, AssetsRepository, ConfigCacher, Database, WalletsRepository, database::devices::DevicesStore, models::{FiatQuoteRequestRow, FiatQuoteRow, NewFiatWebhookRow}, }; @@ -339,7 +339,13 @@ impl FiatClient { Ok(FiatQuotes { quotes, errors }) } - pub async fn get_quote_url_legacy(&self, quote_id: &str, wallet_address: &str, ip_address: &str, device_id: &str) -> Result<(FiatQuoteUrl, FiatQuote), Box> { + pub async fn get_quote_url_legacy( + &self, + quote_id: &str, + wallet_address: &str, + ip_address: &str, + device_id: &str, + ) -> Result<(FiatQuoteUrl, FiatQuote), Box> { let mut client = self.database.client()?; let device = DevicesStore::get_device(&mut client, device_id)?; @@ -410,7 +416,7 @@ impl FiatClient { } fn is_address_subscribed(&self, asset: &Asset, wallet_address: &str) -> Result> { - Ok(self.database.subscriptions()?.get_subscription_address_exists(asset.chain, wallet_address)?) + Ok(self.database.wallets()?.get_subscription_address_exists(asset.chain, wallet_address)?) } fn check_asset_limits_old(request: &FiatQuoteOldRequest, mapping: &FiatMapping) -> Result<(), FiatQuoteError> { diff --git a/crates/gem_aptos/src/provider/staking_mapper.rs b/crates/gem_aptos/src/provider/staking_mapper.rs index b086579a9..2691623c4 100644 --- a/crates/gem_aptos/src/provider/staking_mapper.rs +++ b/crates/gem_aptos/src/provider/staking_mapper.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use num_bigint::BigUint; -use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use crate::models::{DelegationPoolStake, StakingConfig, ValidatorInfo, ValidatorSet}; @@ -21,6 +21,7 @@ pub fn map_validator(validator: &ValidatorInfo, apy: f64, commission: f64, is_ac is_active, commission, apr: apy, + provider_type: GrowthProviderType::Stake, } } diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index a7a096b63..71feec072 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -111,7 +111,7 @@ impl AptosClient { | TransactionInputType::Stake(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Earn(_, _, _) => Ok(1500), + | TransactionInputType::Earn(_, _) => Ok(1500), TransactionInputType::Perpetual(_, _) => unimplemented!(), } } diff --git a/crates/gem_client/src/types.rs b/crates/gem_client/src/types.rs index dabc44145..e3d36130c 100644 --- a/crates/gem_client/src/types.rs +++ b/crates/gem_client/src/types.rs @@ -8,7 +8,7 @@ pub struct Response { pub data: Vec, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum ClientError { Network(String), Timeout, @@ -16,6 +16,20 @@ pub enum ClientError { Serialization(String), } +impl fmt::Debug for ClientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Network(msg) => f.debug_tuple("Network").field(msg).finish(), + Self::Timeout => write!(f, "Timeout"), + Self::Http { status, body } => { + let body_str = String::from_utf8_lossy(&body[..body.len().min(256)]); + f.debug_struct("Http").field("status", status).field("body", &body_str).finish() + } + Self::Serialization(msg) => f.debug_tuple("Serialization").field(msg).finish(), + } + } +} + pub fn decode_json_byte_array(values: Vec) -> Result, ClientError> { let mut bytes = Vec::with_capacity(values.len()); for value in values { @@ -57,10 +71,6 @@ where Ok(value) => Ok(value), Err(error) => { validate_http_status(response)?; - - let preview_bytes = if response.data.len() > 256 { &response.data[..256] } else { &response.data }; - println!("Deserialize error: {}, response: {}", error, String::from_utf8_lossy(preview_bytes)); - Err(ClientError::Serialization(error.to_string())) } } diff --git a/crates/gem_cosmos/src/provider/preload_mapper.rs b/crates/gem_cosmos/src/provider/preload_mapper.rs index d4c503e72..5c4cb45d7 100644 --- a/crates/gem_cosmos/src/provider/preload_mapper.rs +++ b/crates/gem_cosmos/src/provider/preload_mapper.rs @@ -12,7 +12,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(3_000u64), + | TransactionInputType::Earn(_, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(25_000u64), }, @@ -24,7 +24,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(10_000u64), + | TransactionInputType::Earn(_, _) => BigInt::from(10_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(10_000u64), TransactionInputType::Stake(_, _) => BigInt::from(100_000u64), }, @@ -36,7 +36,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(3_000u64), + | TransactionInputType::Earn(_, _) => BigInt::from(3_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(3_000u64), TransactionInputType::Stake(_, _) => BigInt::from(10_000u64), }, @@ -48,7 +48,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(100_000u64), + | TransactionInputType::Earn(_, _) => BigInt::from(100_000u64), TransactionInputType::Swap(_, _, _) => BigInt::from(100_000u64), TransactionInputType::Stake(_, _) => BigInt::from(200_000u64), }, @@ -60,7 +60,7 @@ fn get_fee(chain: CosmosChain, input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(100_000_000_000_000u64), + | TransactionInputType::Earn(_, _) => 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), }, @@ -77,7 +77,7 @@ fn get_gas_limit(input_type: &TransactionInputType, _chain: CosmosChain) -> u64 | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => 200_000, + | TransactionInputType::Earn(_, _) => 200_000, TransactionInputType::Swap(_, _, _) => 200_000, TransactionInputType::Stake(_, operation) => match operation { StakeType::Stake(_) | StakeType::Unstake(_) => 1_000_000, diff --git a/crates/gem_cosmos/src/provider/staking_mapper.rs b/crates/gem_cosmos/src/provider/staking_mapper.rs index fa378eaca..f70b38bd3 100644 --- a/crates/gem_cosmos/src/provider/staking_mapper.rs +++ b/crates/gem_cosmos/src/provider/staking_mapper.rs @@ -11,7 +11,7 @@ use crate::models::{OsmosisDistributionProportions, OsmosisMintParams}; use number_formatter::BigNumberFormatter; use primitives::chain_cosmos::CosmosChain; -use primitives::{DelegationBase, DelegationState, DelegationValidator}; +use primitives::{DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use std::collections::HashMap; const BOND_STATUS_BONDED: &str = "BOND_STATUS_BONDED"; @@ -68,6 +68,7 @@ pub fn map_staking_validators(validators: Vec, chain: CosmosChain, ap is_active, commission: commission_rate * 100.0, apr: validator_apr, + provider_type: GrowthProviderType::Stake, } }) .collect() diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index 287165bc8..430ec1cf0 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -70,14 +70,7 @@ impl EthereumClient { }, _ => input.metadata, }, - TransactionInputType::Earn(_, _, earn_input) => match input.metadata { - TransactionLoadMetadata::Evm { nonce, chain_id, .. } => TransactionLoadMetadata::Evm { - nonce, - chain_id, - earn_data: Some(earn_input.clone()), - }, - _ => input.metadata, - }, + TransactionInputType::Earn(_, _) => input.metadata, _ => input.metadata, }; diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index 8f2f9197b..fce6068ed 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -8,8 +8,7 @@ use num_bigint::BigInt; use num_traits::Num; use primitives::swap::SwapQuoteDataType; use primitives::{ - AssetSubtype, Chain, EVMChain, EarnAction, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, fee::FeePriority, - fee::GasPriceType, + AssetSubtype, Chain, EVMChain, FeeRate, NFTType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, fee::FeePriority, fee::GasPriceType, }; use crate::contracts::{IERC20, IERC721, IERC1155}; @@ -142,18 +141,18 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> } _ => Err("Unsupported chain for staking".into()), }, - TransactionInputType::Earn(_, action, earn_data) => { + TransactionInputType::Earn(_, _) => { + let earn_data = match &input.metadata { + TransactionLoadMetadata::Evm { earn_data, .. } => earn_data.as_ref().ok_or("Missing earn_data in metadata")?, + _ => return Err("EVM metadata required for earn transactions".into()), + }; 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 = 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 { - EarnAction::Deposit => BigInt::from(0), - EarnAction::Withdraw => BigInt::from(0), - }; - Ok(TransactionParams::new(contract_address.clone(), decoded_data, tx_value)) + Ok(TransactionParams::new(contract_address.clone(), decoded_data, BigInt::from(0))) } } _ => Err("Unsupported transfer type".into()), @@ -194,9 +193,14 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { - if let Some(gas_limit) = earn_data.gas_limit.as_ref() - && earn_data.approval.is_some() + TransactionInputType::Earn(_, _) => { + let earn_data = match &input.metadata { + TransactionLoadMetadata::Evm { earn_data, .. } => earn_data.as_ref(), + _ => None, + }; + if let Some(data) = earn_data + && let Some(gas_limit) = data.gas_limit.as_ref() + && data.approval.is_some() { Ok(BigInt::from_str_radix(gas_limit, 10)?) } else { @@ -304,7 +308,7 @@ mod tests { use super::*; use crate::everstake::{EVERSTAKE_POOL_ADDRESS, IAccounting}; use num_bigint::BigUint; - use primitives::{Delegation, DelegationBase, DelegationState, DelegationValidator, RedelegateData}; + use primitives::{Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, RedelegateData}; fn everstake_validator() -> DelegationValidator { DelegationValidator { @@ -314,6 +318,7 @@ mod tests { is_active: true, commission: 10.0, apr: 4.2, + provider_type: GrowthProviderType::Stake, } } @@ -447,6 +452,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: GrowthProviderType::Stake, }; let stake_type = StakeType::Stake(validator); @@ -483,6 +489,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: GrowthProviderType::Stake, }, price: None, }; @@ -520,6 +527,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: GrowthProviderType::Stake, }, price: None, }; @@ -531,6 +539,7 @@ mod tests { is_active: true, commission: 3.0, apr: 12.0, + provider_type: GrowthProviderType::Stake, }; let redelegate_data = RedelegateData { delegation, to_validator }; @@ -568,6 +577,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: GrowthProviderType::Stake, }, price: None, }; diff --git a/crates/gem_evm/src/provider/staking_ethereum.rs b/crates/gem_evm/src/provider/staking_ethereum.rs index 6111a1646..d4cb93572 100644 --- a/crates/gem_evm/src/provider/staking_ethereum.rs +++ b/crates/gem_evm/src/provider/staking_ethereum.rs @@ -1,7 +1,7 @@ use gem_client::Client; use num_bigint::BigUint; use num_traits::Zero; -use primitives::{AssetBalance, AssetId, Balance, Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{AssetBalance, AssetId, Balance, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use std::error::Error; use crate::everstake::{EVERSTAKE_POOL_ADDRESS, get_everstake_account_state, map_balance_to_delegation, map_withdraw_request_to_delegations}; @@ -32,6 +32,7 @@ impl EthereumClient { is_active: true, commission: 0.1, apr: apy, + provider_type: GrowthProviderType::Stake, }]) } diff --git a/crates/gem_evm/src/provider/staking_monad.rs b/crates/gem_evm/src/provider/staking_monad.rs index e61bb1b09..582ea9254 100644 --- a/crates/gem_evm/src/provider/staking_monad.rs +++ b/crates/gem_evm/src/provider/staking_monad.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; use gem_client::Client; use num_bigint::BigUint; use num_traits::{ToPrimitive, Zero}; -use primitives::{AssetBalance, AssetId, Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{AssetBalance, AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use crate::monad::{ IMonadStakingLens, MONAD_SCALE, MonadLensBalance, MonadLensDelegation, MonadLensValidatorInfo, STAKING_LENS_CONTRACT, decode_get_lens_apys, decode_get_lens_balance, @@ -124,6 +124,7 @@ impl EthereumClient { is_active: validator.is_active, commission: Self::lens_commission_rate(&validator.commission), apr: if validator.apy_bps > 0 { validator.apy_bps as f64 / 100.0 } else { network_apy }, + provider_type: GrowthProviderType::Stake, } } diff --git a/crates/gem_evm/src/provider/staking_smartchain.rs b/crates/gem_evm/src/provider/staking_smartchain.rs index 66f6a4b9a..6c4459425 100644 --- a/crates/gem_evm/src/provider/staking_smartchain.rs +++ b/crates/gem_evm/src/provider/staking_smartchain.rs @@ -7,7 +7,7 @@ use gem_bsc::stake_hub::{ }; use gem_client::Client; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use std::{error::Error, str::FromStr}; #[cfg(feature = "rpc")] @@ -37,6 +37,7 @@ impl EthereumClient { is_active: !v.jailed, commission: v.commission as f64 / 10000.0, apr: v.apy as f64 / 100.0, + provider_type: GrowthProviderType::Stake, }) .collect()) } diff --git a/crates/gem_hypercore/src/models/candlestick.rs b/crates/gem_hypercore/src/models/candlestick.rs index d7076b6d3..ebd7182d0 100644 --- a/crates/gem_hypercore/src/models/candlestick.rs +++ b/crates/gem_hypercore/src/models/candlestick.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use primitives::chart::ChartCandleStick; +use primitives::chart::{ChartCandleStick, ChartCandleUpdate}; use serde::{Deserialize, Serialize}; use crate::models::UInt64; @@ -7,6 +7,7 @@ use crate::models::UInt64; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Candlestick { pub t: UInt64, // Open time (timestamp in milliseconds) + pub s: String, // Symbol (coin) pub i: String, // Interval pub o: String, // Open price pub h: String, // High price @@ -15,16 +16,25 @@ pub struct Candlestick { pub v: String, // Volume } -impl From for ChartCandleStick { - fn from(candlestick: Candlestick) -> Self { +impl From<&Candlestick> for ChartCandleStick { + fn from(c: &Candlestick) -> Self { ChartCandleStick { - date: DateTime::from_timestamp(candlestick.t as i64 / 1000, 0).unwrap_or(Utc::now()), - interval: candlestick.i, - open: candlestick.o.parse().unwrap_or(0.0), - high: candlestick.h.parse().unwrap_or(0.0), - low: candlestick.l.parse().unwrap_or(0.0), - close: candlestick.c.parse().unwrap_or(0.0), - volume: candlestick.v.parse().unwrap_or(0.0), + date: DateTime::from_timestamp(c.t as i64 / 1000, 0).unwrap_or(Utc::now()), + open: c.o.parse().unwrap_or(0.0), + high: c.h.parse().unwrap_or(0.0), + low: c.l.parse().unwrap_or(0.0), + close: c.c.parse().unwrap_or(0.0), + volume: c.v.parse().unwrap_or(0.0), + } + } +} + +impl From for ChartCandleUpdate { + fn from(c: Candlestick) -> Self { + ChartCandleUpdate { + coin: c.s.clone(), + interval: c.i.clone(), + candle: ChartCandleStick::from(&c), } } } diff --git a/crates/gem_hypercore/src/models/websocket.rs b/crates/gem_hypercore/src/models/websocket.rs index 77ab30e3a..28eb1d822 100644 --- a/crates/gem_hypercore/src/models/websocket.rs +++ b/crates/gem_hypercore/src/models/websocket.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use primitives::PerpetualPosition; -use primitives::chart::ChartCandleStick; +use primitives::chart::ChartCandleUpdate; use primitives::perpetual::PerpetualBalance; use serde::Deserialize; @@ -70,7 +70,7 @@ pub struct PositionsDiff { pub enum HyperliquidSocketMessage { ClearinghouseState { balance: PerpetualBalance, positions: Vec }, OpenOrders { orders: Vec }, - Candle { candle: ChartCandleStick }, + Candle { candle: ChartCandleUpdate }, AllMids { prices: HashMap }, SubscriptionResponse { subscription_type: String }, Unknown, diff --git a/crates/gem_hypercore/src/provider/balances.rs b/crates/gem_hypercore/src/provider/balances.rs index 8e8e9adf6..8d4aa8ed1 100644 --- a/crates/gem_hypercore/src/provider/balances.rs +++ b/crates/gem_hypercore/src/provider/balances.rs @@ -10,6 +10,8 @@ use primitives::{Asset, AssetBalance}; use super::balances_mapper::{map_balance_coin, map_balance_staking, map_balance_tokens}; use crate::rpc::client::HyperCoreClient; +const NATIVE_TOKEN_INDEX: u32 = 150; + #[async_trait] impl ChainBalances for HyperCoreClient { async fn get_balance_coin(&self, address: String) -> Result> { @@ -18,9 +20,9 @@ impl ChainBalances for HyperCoreClient { .await? .balances .into_iter() - .find(|x| x.token == 150) - .ok_or("not found")? - .total; + .find(|balance| balance.token == NATIVE_TOKEN_INDEX) + .map(|balance| balance.total) + .unwrap_or_else(|| "0".to_string()); let native_decimals = Asset::from_chain(self.chain).decimals as u32; let available: String = BigNumberFormatter::value_from_amount(&total, native_decimals)?; Ok(map_balance_coin(available, self.chain)) diff --git a/crates/gem_hypercore/src/provider/perpetual_mapper.rs b/crates/gem_hypercore/src/provider/perpetual_mapper.rs index 458c07108..735914b2e 100644 --- a/crates/gem_hypercore/src/provider/perpetual_mapper.rs +++ b/crates/gem_hypercore/src/provider/perpetual_mapper.rs @@ -142,7 +142,7 @@ pub fn map_perpetuals_data(metadata: HypercoreMetadataResponse) -> Vec) -> Vec { - candlesticks.into_iter().map(|c| c.into()).collect() + candlesticks.iter().map(ChartCandleStick::from).collect() } pub fn map_account_summary(positions: &AssetPositions) -> PerpetualAccountSummary { @@ -334,6 +334,7 @@ mod tests { let candlesticks = vec![ Candlestick { t: 1640995200000u64, // 2022-01-01 00:00:00 UTC + s: "BTC".to_string(), i: "1h".to_string(), o: "50000.0".to_string(), h: "51000.0".to_string(), @@ -343,6 +344,7 @@ mod tests { }, Candlestick { t: 1640998800000u64, // 2022-01-01 01:00:00 UTC + s: "BTC".to_string(), i: "1h".to_string(), o: "50500.0".to_string(), h: "52000.0".to_string(), diff --git a/crates/gem_hypercore/src/provider/staking_mapper.rs b/crates/gem_hypercore/src/provider/staking_mapper.rs index 41fca6c59..d5b4c3f1d 100644 --- a/crates/gem_hypercore/src/provider/staking_mapper.rs +++ b/crates/gem_hypercore/src/provider/staking_mapper.rs @@ -1,7 +1,7 @@ use crate::models::balance::{DelegationBalance, Validator}; use num_bigint::BigUint; use number_formatter::BigNumberFormatter; -use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use std::str::FromStr; pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Option) -> Vec { @@ -15,6 +15,7 @@ pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Opt is_active: x.is_active, commission: x.commission, apr: calculated_apy, + provider_type: GrowthProviderType::Stake, }) .collect() } diff --git a/crates/gem_hypercore/src/provider/websocket_mapper.rs b/crates/gem_hypercore/src/provider/websocket_mapper.rs index e31b6c9d0..d5e65cba2 100644 --- a/crates/gem_hypercore/src/provider/websocket_mapper.rs +++ b/crates/gem_hypercore/src/provider/websocket_mapper.rs @@ -98,16 +98,17 @@ mod tests { #[test] fn test_parse_candle() { let json = include_bytes!("../../testdata/ws_candle.json"); - let HyperliquidSocketMessage::Candle { candle } = parse_websocket_data(json).unwrap() else { + let HyperliquidSocketMessage::Candle { candle: update } = parse_websocket_data(json).unwrap() else { panic!("expected Candle"); }; - assert_eq!(candle.interval, "1h"); - assert_eq!(candle.open, 3300.5); - assert_eq!(candle.close, 3321.1); - assert_eq!(candle.high, 3345.0); - assert_eq!(candle.low, 3290.2); - assert_eq!(candle.volume, 12450.8); + assert_eq!(update.coin, "ETH"); + assert_eq!(update.interval, "1h"); + assert_eq!(update.candle.open, 3300.5); + assert_eq!(update.candle.close, 3321.1); + assert_eq!(update.candle.high, 3345.0); + assert_eq!(update.candle.low, 3290.2); + assert_eq!(update.candle.volume, 12450.8); } #[test] diff --git a/crates/gem_hypercore/src/signer/core_signer.rs b/crates/gem_hypercore/src/signer/core_signer.rs index 627aa1d22..321f7f67b 100644 --- a/crates/gem_hypercore/src/signer/core_signer.rs +++ b/crates/gem_hypercore/src/signer/core_signer.rs @@ -428,8 +428,8 @@ mod tests { use crate::core::actions::Grouping; use num_bigint::{BigInt, BigUint}; use primitives::{ - Asset, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GasPriceType, StakeType, TransactionInputType, TransactionLoadInput, - TransactionLoadMetadata, + Asset, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, GasPriceType, StakeType, TransactionInputType, + TransactionLoadInput, TransactionLoadMetadata, }; #[test] @@ -443,6 +443,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, + provider_type: GrowthProviderType::Stake, }; let input = TransactionLoadInput { input_type: TransactionInputType::Stake(asset.clone(), StakeType::Stake(validator)), @@ -497,6 +498,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, + provider_type: GrowthProviderType::Stake, }, price: None, }; diff --git a/crates/gem_jsonrpc/src/types.rs b/crates/gem_jsonrpc/src/types.rs index 33355d45e..771272def 100644 --- a/crates/gem_jsonrpc/src/types.rs +++ b/crates/gem_jsonrpc/src/types.rs @@ -67,13 +67,36 @@ pub struct JsonRpcErrorResponse { pub error: JsonRpcError, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(untagged)] pub enum JsonRpcResult { Value(JsonRpcResponse), Error(JsonRpcErrorResponse), } +impl<'de, T: Deserialize<'de>> Deserialize<'de> for JsonRpcResult { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = Value::deserialize(deserializer)?; + let id = raw.get("id").and_then(|v| v.as_u64()); + + if let Some(error) = raw.get("error") { + let error: JsonRpcError = serde_json::from_value(error.clone()).map_err(serde::de::Error::custom)?; + return Ok(JsonRpcResult::Error(JsonRpcErrorResponse { id, error })); + } + + let Some(result) = raw.get("result") else { + return Err(serde::de::Error::custom(format!("missing result and error fields, raw: {raw}"))); + }; + + let result = + T::deserialize(result.clone()).map_err(|e| serde::de::Error::custom(format!("failed to deserialize result: {e}, raw: {result}")))?; + Ok(JsonRpcResult::Value(JsonRpcResponse { id, result })) + } +} + impl JsonRpcResult { pub fn take(self) -> Result { match self { @@ -146,4 +169,51 @@ mod tests { assert_eq!(format!("{error}"), "Method not found (-32601)"); } + + #[derive(Debug, Deserialize, PartialEq)] + struct Block { + number: String, + } + + #[test] + fn test_deserialize_success() { + let json = r#"{"id": 1, "result": {"number": "0x10"}}"#; + let result: JsonRpcResult = serde_json::from_str(json).unwrap(); + assert!(matches!(result, JsonRpcResult::Value(r) if r.result.number == "0x10")); + } + + #[test] + fn test_deserialize_error_response() { + let json = r#"{"id": 1, "error": {"code": -32601, "message": "Method not found"}}"#; + let result: JsonRpcResult = serde_json::from_str(json).unwrap(); + assert!(matches!(result, JsonRpcResult::Error(e) if e.error.code == -32601)); + } + + #[test] + fn test_deserialize_null_result_fails_with_detail() { + let json = r#"{"id": 1, "result": null}"#; + let err = serde_json::from_str::>(json).unwrap_err(); + assert!( + err.to_string() + .contains("failed to deserialize result: invalid type: null, expected struct Block, raw: null") + ); + } + + #[test] + fn test_deserialize_null_result_ok_for_option() { + let json = r#"{"id": 1, "result": null}"#; + let result: JsonRpcResult> = serde_json::from_str(json).unwrap(); + assert!(matches!(result, JsonRpcResult::Value(r) if r.result.is_none())); + } + + #[test] + fn test_deserialize_batch_with_mixed_results() { + let json = r#"[ + {"id": 1, "result": {"number": "0x10"}}, + {"id": 2, "error": {"code": -32600, "message": "Invalid"}} + ]"#; + let results: Vec> = serde_json::from_str(json).unwrap(); + assert!(matches!(&results[0], JsonRpcResult::Value(_))); + assert!(matches!(&results[1], JsonRpcResult::Error(_))); + } } diff --git a/crates/gem_near/src/provider/balances.rs b/crates/gem_near/src/provider/balances.rs index 31534ee90..0018dedef 100644 --- a/crates/gem_near/src/provider/balances.rs +++ b/crates/gem_near/src/provider/balances.rs @@ -3,15 +3,22 @@ use chain_traits::ChainBalances; use std::error::Error; use gem_client::Client; -use primitives::AssetBalance; +use gem_jsonrpc::types::JsonRpcError; +use primitives::{AssetBalance, Chain}; use super::balances_mapper; use crate::rpc::client::NearClient; +const ACCOUNT_NOT_FOUND_ERROR_CODE: i32 = -32000; + #[async_trait] impl ChainBalances for NearClient { async fn get_balance_coin(&self, address: String) -> Result> { - let account = self.get_account(&address).await?; + let account = match self.get_account(&address).await { + Ok(account) => account, + Err(error) if is_account_missing(&error) => return Ok(AssetBalance::new_zero_balance(Chain::Near.as_asset_id())), + Err(error) => return Err(error.into()), + }; balances_mapper::map_native_balance(&account) } @@ -28,6 +35,10 @@ impl ChainBalances for NearClient { } } +fn is_account_missing(error: &JsonRpcError) -> bool { + error.code == ACCOUNT_NOT_FOUND_ERROR_CODE +} + #[cfg(all(test, feature = "chain_integration_tests"))] mod chain_integration_tests { use crate::provider::testkit::{TEST_ADDRESS, create_near_test_client}; diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index dc42feefa..3cf670ddf 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -42,7 +42,7 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => BigInt::from(100_000), + | TransactionInputType::Earn(_, _) => BigInt::from(100_000), TransactionInputType::Swap(_, _, _) => BigInt::from(420_000), TransactionInputType::Stake(_, _) => BigInt::from(100_000), } @@ -57,7 +57,7 @@ fn get_multiple_of(input_type: &TransactionInputType) -> i64 { | TransactionInputType::TokenApprove(asset, _) | TransactionInputType::Generic(asset, _, _) | TransactionInputType::Perpetual(asset, _) - | TransactionInputType::Earn(asset, _, _) => match &asset.id.token_subtype() { + | TransactionInputType::Earn(asset, _) => match &asset.id.token_subtype() { AssetSubtype::NATIVE => 25_000, AssetSubtype::TOKEN => 50_000, }, diff --git a/crates/gem_solana/src/provider/staking_mapper.rs b/crates/gem_solana/src/provider/staking_mapper.rs index 2ee98538b..9c3f02f7a 100644 --- a/crates/gem_solana/src/provider/staking_mapper.rs +++ b/crates/gem_solana/src/provider/staking_mapper.rs @@ -1,7 +1,7 @@ use crate::models::{EpochInfo, TokenAccountInfo, VoteAccount}; use chrono::Utc; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; pub fn map_staking_validators(vote_accounts: Vec, chain: Chain, network_apy: f64) -> Vec { vote_accounts @@ -18,6 +18,7 @@ pub fn map_staking_validators(vote_accounts: Vec, chain: Chain, net is_active, commission: validator.commission as f64, apr: validator_apr, + provider_type: GrowthProviderType::Stake, } }) .collect() diff --git a/crates/gem_sui/src/provider/preload_mapper.rs b/crates/gem_sui/src/provider/preload_mapper.rs index e397570c9..b04aa7d29 100644 --- a/crates/gem_sui/src/provider/preload_mapper.rs +++ b/crates/gem_sui/src/provider/preload_mapper.rs @@ -37,7 +37,7 @@ fn get_gas_limit(input_type: &TransactionInputType) -> u64 { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) - | TransactionInputType::Earn(_, _, _) => GAS_BUDGET, + | TransactionInputType::Earn(_, _) => GAS_BUDGET, TransactionInputType::Swap(_, _, _) => 50_000_000, TransactionInputType::Stake(_, _) => GAS_BUDGET, } diff --git a/crates/gem_sui/src/provider/staking_mapper.rs b/crates/gem_sui/src/provider/staking_mapper.rs index e16d3dfee..0ff96f177 100644 --- a/crates/gem_sui/src/provider/staking_mapper.rs +++ b/crates/gem_sui/src/provider/staking_mapper.rs @@ -2,7 +2,7 @@ use crate::models::RpcSuiSystemState; use crate::models::staking::{SuiStakeDelegation, SuiSystemState, SuiValidators}; use chrono::{DateTime, Utc}; use num_bigint::BigUint; -use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, StakeValidator}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, StakeValidator}; pub fn map_validators(validators: SuiValidators, default_apy: f64) -> Vec { validators @@ -15,6 +15,7 @@ pub fn map_validators(validators: SuiValidators, default_apy: f64) -> Vec ChainTransactionLoad for TronClient { } async fn get_transaction_load(&self, input: TransactionLoadInput) -> Result> { - let (block, chain_parameters, account_usage, is_new_account, votes) = futures::try_join!( + let (block, chain_parameters, account_usage, is_new_account, stake_data) = futures::try_join!( self.get_tron_block(), self.get_chain_parameters(), self.get_account_usage(&input.sender_address), self.get_is_new_account_for_input_type(&input.destination_address, input.input_type.clone()), - self.get_votes_for_transaction_input(&input) + self.get_stake_data(&input) )?; let block = block.block_header.raw_data; @@ -42,7 +43,7 @@ impl ChainTransactionLoad for TronClient { transaction_tree_root: block.tx_trie_root.clone(), parent_hash: block.parent_hash.clone(), witness_address: block.witness_address.clone(), - votes, + stake_data, }; let fee = match &input.input_type { @@ -122,36 +123,44 @@ impl TronClient { } } - async fn get_votes_for_transaction_input(&self, input: &TransactionLoadInput) -> Result, Box> { + async fn get_stake_data(&self, input: &TransactionLoadInput) -> Result> { match &input.input_type { TransactionInputType::Stake(asset, stake_type) => { let account = self.get_account(&input.sender_address).await?; - let mut current_votes: HashMap = account.votes.unwrap_or_default().into_iter().map(|v| (v.vote_address, v.vote_count)).collect(); - - let vote_amount = input.value.parse::().unwrap_or(0) / 10_u64.pow(asset.decimals as u32); + let amount = BigNumberFormatter::value_as_u64(&input.value, asset.decimals as u32)?; + let mut votes: HashMap = account + .votes + .as_ref() + .map(|v| v.iter().map(|v| (v.vote_address.clone(), v.vote_count)).collect()) + .unwrap_or_default(); match stake_type { - StakeType::Stake(validator) => { - *current_votes.entry(validator.id.clone()).or_insert(0) += vote_amount; - } - StakeType::Unstake(delegation) => { - if let Some(votes) = current_votes.get_mut(&delegation.base.validator_id) { - *votes = votes.saturating_sub(vote_amount); + StakeType::Stake(v) => *votes.entry(v.id.clone()).or_default() += amount, + StakeType::Unstake(d) => { + votes.entry(d.base.validator_id.clone()).and_modify(|v| *v = v.saturating_sub(amount)); + votes.retain(|_, v| *v > 0); + if votes.is_empty() { + return Ok(TronStakeData::Unfreeze(calculate_unfreeze_amounts( + account.frozen_v2.as_ref(), + BigNumberFormatter::value_as_u64(&input.value, 0)?, + ))); } } - StakeType::Redelegate(redelegate_data) => { - if let Some(votes) = current_votes.get_mut(&redelegate_data.delegation.base.validator_id) { - *votes = votes.saturating_sub(vote_amount); - } - *current_votes.entry(redelegate_data.to_validator.id.clone()).or_insert(0) += vote_amount; + StakeType::Redelegate(r) => { + votes.entry(r.delegation.base.validator_id.clone()).and_modify(|v| *v = v.saturating_sub(amount)); + *votes.entry(r.to_validator.id.clone()).or_default() += amount; } StakeType::Rewards(_) | StakeType::Withdraw(_) | StakeType::Freeze(_) => {} } - current_votes.retain(|_, &mut v| v > 0); - Ok(current_votes) + let votes = votes + .into_iter() + .filter(|(_, count)| *count > 0) + .map(|(validator, count)| TronVote { validator, count }) + .collect(); + Ok(TronStakeData::Votes(votes)) } - _ => Ok(HashMap::new()), + _ => Ok(TronStakeData::Votes(vec![])), } } } diff --git a/crates/gem_tron/src/provider/preload_mapper.rs b/crates/gem_tron/src/provider/preload_mapper.rs index a4c5cedc3..68a2443bf 100644 --- a/crates/gem_tron/src/provider/preload_mapper.rs +++ b/crates/gem_tron/src/provider/preload_mapper.rs @@ -4,8 +4,9 @@ use num_bigint::BigInt; use crate::models::ChainParameter; use crate::models::TronAccountUsage; +use crate::models::account::TronFrozen; use crate::rpc::constants::{DEFAULT_BANDWIDTH_BYTES, GET_CREATE_ACCOUNT_FEE, GET_CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT, GET_ENERGY_FEE, GET_TRANSACTION_FEE}; -use primitives::StakeType; +use primitives::{Resource, StakeType, TronUnfreeze}; const FEE_LIMIT_BUFFER_PERCENT: u64 = 20; @@ -85,6 +86,30 @@ fn get_chain_parameter_value(parameters: &[ChainParameter], key: &str) -> Result .ok_or_else(|| format!("Missing chain parameter: {}", key).into()) } +pub fn calculate_unfreeze_amounts(frozen: Option<&Vec>, total: u64) -> Vec { + frozen + .map(|frozen| { + frozen + .iter() + .filter(|f| f.amount > 0) + .scan(total, |remaining, f| { + (*remaining > 0).then(|| { + let take = (*remaining).min(f.amount); + *remaining -= take; + TronUnfreeze { + resource: match f.frozen_type.as_deref() { + Some("ENERGY") => Resource::Energy, + _ => Resource::Bandwidth, + }, + amount: take, + } + }) + }) + .collect() + }) + .unwrap_or_default() +} + impl TronAccountUsage { pub fn available_bandwidth(&self) -> u64 { let free = self.free_net_limit.saturating_sub(self.free_net_used); @@ -108,7 +133,9 @@ impl TronAccountUsage { #[cfg(test)] mod tests { use super::*; + use crate::models::account::TronFrozen; use primitives::Chain; + use primitives::GrowthProviderType; use primitives::delegation::DelegationValidator; fn chain_parameter(key: &str, value: i64) -> ChainParameter { @@ -251,6 +278,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, + provider_type: GrowthProviderType::Stake, }); let with_bandwidth = account_usage(DEFAULT_BANDWIDTH_BYTES, 0, 0); @@ -285,4 +313,41 @@ mod tests { assert_eq!(fee.fee, 77142 * 420 + DEFAULT_BANDWIDTH_BYTES * 1000); assert_eq!(fee.fee_limit, 77142 * 420); } + + #[test] + fn test_calculate_unfreeze_amounts() { + let frozen = vec![ + TronFrozen { + frozen_type: Some("ENERGY".to_string()), + amount: 100, + }, + TronFrozen { + frozen_type: Some("BANDWIDTH".to_string()), + amount: 50, + }, + ]; + + assert_eq!( + calculate_unfreeze_amounts(Some(&frozen), 120), + vec![ + TronUnfreeze { + resource: Resource::Energy, + amount: 100 + }, + TronUnfreeze { + resource: Resource::Bandwidth, + amount: 20 + }, + ] + ); + assert_eq!( + calculate_unfreeze_amounts(Some(&frozen), 50), + vec![TronUnfreeze { + resource: Resource::Energy, + amount: 50 + },] + ); + assert!(calculate_unfreeze_amounts(None, 100).is_empty()); + assert!(calculate_unfreeze_amounts(Some(&frozen), 0).is_empty()); + } } diff --git a/crates/gem_tron/src/provider/staking_mapper.rs b/crates/gem_tron/src/provider/staking_mapper.rs index 6b582fa35..75e94090a 100644 --- a/crates/gem_tron/src/provider/staking_mapper.rs +++ b/crates/gem_tron/src/provider/staking_mapper.rs @@ -1,6 +1,6 @@ use crate::address::TronAddress; use crate::models::WitnessesList; -use primitives::{Chain, DelegationValidator, StakeValidator}; +use primitives::{Chain, DelegationValidator, GrowthProviderType, StakeValidator}; const SYSTEM_UNSTAKING_VALIDATOR_ID: &str = "system"; const SYSTEM_UNSTAKING_VALIDATOR_NAME: &str = "Unstaking"; @@ -22,6 +22,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option) -> Vec is_active: witness.is_jobs.unwrap_or(false), commission: 0.0, apr: default_apy, + provider_type: GrowthProviderType::Stake, }) }) .collect(); @@ -33,6 +34,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option) -> Vec is_active: true, commission: 0.0, apr: default_apy, + provider_type: GrowthProviderType::Stake, }); validators diff --git a/crates/job_runner/src/lib.rs b/crates/job_runner/src/lib.rs index 3a41d0a39..8acf22773 100644 --- a/crates/job_runner/src/lib.rs +++ b/crates/job_runner/src/lib.rs @@ -1,10 +1,10 @@ use std::fmt::Debug; use std::future::Future; -use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; +use async_trait::async_trait; use gem_tracing::{error_with_fields, info_with_fields}; pub mod schedule; pub use schedule::{JobSchedule, RunAlways, RunDecision}; @@ -15,8 +15,9 @@ use tokio::time::{Duration, Instant}; pub type ShutdownReceiver = watch::Receiver; pub type JobError = Box; +#[async_trait] pub trait JobStatusReporter: Send + Sync { - fn report(&self, name: &str, interval: u64, duration: u64, success: bool, error: Option) -> Pin + Send + '_>>; + async fn report(&self, name: &str, interval: u64, duration: u64, success: bool, error: Option); } pub async fn sleep_or_shutdown(duration: Duration, shutdown_rx: &ShutdownReceiver) -> bool { @@ -28,9 +29,8 @@ pub async fn sleep_or_shutdown(duration: Duration, shutdown_rx: &ShutdownReceive } fn human_duration(duration: Duration) -> String { - let ms = duration.as_millis(); - if ms == 0 { - return "0ms".to_string(); + if duration.is_zero() { + return "0s".to_string(); } let mut parts = Vec::new(); @@ -48,7 +48,7 @@ fn human_duration(duration: Duration) -> String { } } - if parts.is_empty() { format!("{ms}ms") } else { parts.join(" ") } + if parts.is_empty() { format!("{}ms", duration.subsec_millis()) } else { parts.join(" ") } } pub async fn run_job( @@ -205,19 +205,26 @@ mod tests { use std::time::Duration; #[test] - fn human_duration_sub_second() { - assert_eq!(human_duration(Duration::from_millis(500)), "500ms"); + fn duration_zero() { + assert_eq!(human_duration(Duration::ZERO), "0s"); } #[test] - fn human_duration_seconds_minutes() { + fn duration_sub_second() { + assert_eq!(human_duration(Duration::from_millis(250)), "250ms"); + } + + #[test] + fn duration_seconds_and_minutes() { assert_eq!(human_duration(Duration::from_secs(12)), "12s"); assert_eq!(human_duration(Duration::from_secs(90)), "1m 30s"); + assert_eq!(human_duration(Duration::from_secs(65)), "1m 5s"); } #[test] - fn human_duration_hours_days() { + fn duration_hours_and_days() { assert_eq!(human_duration(Duration::from_secs(3_600 * 5 + 42)), "5h 42s"); assert_eq!(human_duration(Duration::from_secs(86_400 + 3_600 * 2)), "1d 2h"); + assert_eq!(human_duration(Duration::from_secs(90_000)), "1d 1h"); } } diff --git a/crates/name_resolver/Cargo.toml b/crates/name_resolver/Cargo.toml index ce15385d0..7c1aa174d 100644 --- a/crates/name_resolver/Cargo.toml +++ b/crates/name_resolver/Cargo.toml @@ -13,7 +13,7 @@ borsh = { workspace = true } hex = { workspace = true } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } -alloy-ens = { version = "1.5.2" } +alloy-ens = { version = "1.6.1" } gem_client = { path = "../gem_client", features = ["reqwest"] } gem_jsonrpc = { path = "../gem_jsonrpc", features = ["client"] } idna = { version = "1.1.0" } diff --git a/crates/nft/src/client.rs b/crates/nft/src/client.rs index 2ffc9a1fd..488092fa1 100644 --- a/crates/nft/src/client.rs +++ b/crates/nft/src/client.rs @@ -4,27 +4,27 @@ use std::sync::Arc; use primitives::{Chain, NFTAsset, NFTAssetId, NFTCollection, NFTCollectionId, NFTData}; use storage::database::devices::DevicesStore; -use storage::{Database, NftRepository, SubscriptionsRepository, WalletsRepository}; +use storage::{Database, NftRepository, WalletsRepository}; use crate::NFTProviderConfig; use crate::factory::NFTProviderFactory; use crate::image_fetcher::ImageFetcher; -use crate::provider::NFTProviderClient; +use crate::provider::NFTProviders; pub struct NFTClient { database: Database, - provider_client: NFTProviderClient, + providers: NFTProviders, image_fetcher: Arc, } impl NFTClient { pub fn new(database: Database, config: NFTProviderConfig) -> Self { let providers = NFTProviderFactory::new_providers(config); - let provider_client = NFTProviderClient::new(providers); + let providers = NFTProviders::new(providers); Self { database, - provider_client, + providers, image_fetcher: Arc::new(ImageFetcher::new()), } } @@ -37,12 +37,6 @@ impl NFTClient { Ok(true) } - pub async fn get_nft_assets(&self, device_id: &str, wallet_index: i32) -> Result, Box> { - let subscriptions = self.get_subscriptions(device_id, wallet_index)?; - let addresses: HashMap = subscriptions.into_iter().map(|x| (x.chain, x.address)).collect(); - self.fetch_assets_for_addresses(addresses).await - } - pub async fn get_nft_assets_by_wallet_id(&self, device_id: i32, wallet_id: i32) -> Result, Box> { let subscriptions = self.database.wallets()?.get_subscriptions_by_wallet_id(device_id, wallet_id)?; let addresses: HashMap = subscriptions.into_iter().map(|(sub, addr)| (sub.chain.0, addr.address)).collect(); @@ -63,9 +57,8 @@ impl NFTClient { let mut collections = Vec::new(); for collection_id in missing_collection_ids { - match self.provider_client.get_collection(collection_id.clone()).await { - Ok(collection) => collections.push(collection), - Err(e) => println!("nft preload collection {} error: {e}", collection_id.id()), + if let Some(collection) = self.providers.get_collection(collection_id.clone()).await { + collections.push(collection); } } let new_collections = collections.clone().into_iter().map(storage::models::NewNftCollectionRow::from_primitive).collect(); @@ -95,9 +88,8 @@ impl NFTClient { let mut assets = Vec::new(); for asset_id in missing_asset_ids { - match self.provider_client.get_asset(asset_id).await { - Ok(asset) => assets.push(asset), - Err(e) => println!("nft preload asset error: {e}"), + if let Some(asset) = self.providers.get_asset(asset_id).await { + assets.push(asset); } } let new_assets = assets.clone().into_iter().clone().map(storage::models::NftAssetRow::from_primitive).collect::>(); @@ -107,13 +99,9 @@ impl NFTClient { Ok(assets) } - pub fn get_subscriptions(&self, device_id: &str, wallet_index: i32) -> Result, Box> { - Ok(self.database.subscriptions()?.get_subscriptions_by_device_id(device_id, Some(wallet_index))?) - } - pub async fn get_nft_assets_by_chain(&self, chain: Chain, address: &str) -> Result, Box> { let addresses = [(chain, address.to_string())]; - let assets = self.provider_client.get_assets(addresses.into()).await?; + let assets = self.providers.get_assets(addresses.into()).await; self.preload(assets).await } @@ -167,8 +155,8 @@ impl NFTClient { } pub async fn fetch_assets_for_addresses(&self, addresses: HashMap) -> Result, Box> { - let asset_ids = self.provider_client.get_assets(addresses).await?; - self.preload(asset_ids.clone()).await + let asset_ids = self.providers.get_assets(addresses).await; + self.preload(asset_ids).await } pub fn report_nft(&self, device_id: &str, collection_id: String, asset_id: Option, reason: Option) -> Result> { diff --git a/crates/nft/src/lib.rs b/crates/nft/src/lib.rs index 64502bc52..6d1254866 100644 --- a/crates/nft/src/lib.rs +++ b/crates/nft/src/lib.rs @@ -13,5 +13,5 @@ pub use client::NFTClient; pub use config::NFTProviderConfig; pub use factory::NFTProviderFactory; pub use image_fetcher::ImageFetcher; -pub use provider::{NFTProvider, NFTProviderClient}; +pub use provider::{NFTProvider, NFTProviders}; pub use providers::{MagicEdenEvmClient, MagicEdenSolanaClient, OpenSeaClient}; diff --git a/crates/nft/src/provider.rs b/crates/nft/src/provider.rs index da7bdaa3b..58b719d30 100644 --- a/crates/nft/src/provider.rs +++ b/crates/nft/src/provider.rs @@ -3,62 +3,71 @@ use std::error::Error; use std::sync::Arc; use async_trait::async_trait; -use primitives::{Chain, NFTAsset, NFTAssetId, NFTCollection, NFTCollectionId}; +use primitives::{Chain, NFTAsset, NFTAssetId, NFTChain, NFTCollection, NFTCollectionId}; #[async_trait] pub trait NFTProvider: Send + Sync { fn name(&self) -> &'static str; - fn get_chains(&self) -> Vec; + fn chains(&self) -> &'static [NFTChain]; async fn get_assets(&self, chain: Chain, address: String) -> Result, Box>; async fn get_collection(&self, collection: NFTCollectionId) -> Result>; async fn get_asset(&self, asset_id: NFTAssetId) -> Result>; } -#[allow(unused)] -pub struct NFTProviderClient { +pub struct NFTProviders { providers: Vec>, } -impl NFTProviderClient { +impl NFTProviders { pub fn new(providers: Vec>) -> Self { Self { providers } } - pub fn get_provider_for_chain(&self, chain: Chain) -> Result, Box> { + fn providers_for_chain(&self, chain: Chain) -> impl Iterator> { self.providers .iter() - .find(|provider| provider.get_chains().contains(&chain)) - .cloned() - .ok_or_else(|| format!("No provider available for chain: {:?}", chain).into()) + .filter(move |provider| provider.chains().iter().any(|nft_chain| Chain::from(*nft_chain) == chain)) } - pub async fn get_assets(&self, addresses: HashMap) -> Result, Box> { - let futures: Vec<_> = addresses - .into_iter() - .map(|(chain, address)| { - let address = address.clone(); - async move { self.get_asset_ids(chain, address.as_str()).await } - }) - .collect(); + async fn fetch_assets(chain: Chain, address: String, providers: impl Iterator>) -> Vec { + for provider in providers { + if let Ok(ids) = provider.get_assets(chain, address.clone()).await { + return ids; + } + } + vec![] + } + + pub async fn get_assets(&self, addresses: HashMap) -> Vec { + let futures = addresses.into_iter().map(|(chain, address)| { + let providers = self.providers_for_chain(chain); + async move { Self::fetch_assets(chain, address, providers).await } + }); - Ok(futures::future::try_join_all(futures).await?.into_iter().flatten().collect::>()) + futures::future::join_all(futures).await.into_iter().flatten().collect() } - pub async fn get_asset_ids(&self, chain: Chain, address: &str) -> Result, Box> { - match self.get_provider_for_chain(chain) { - Ok(provider) => provider.get_assets(chain, address.to_string()).await, - Err(_) => Ok(vec![]), // Return empty vector for unsupported chains - } + pub async fn get_asset_ids(&self, chain: Chain, address: &str) -> Vec { + let providers = self.providers_for_chain(chain); + Self::fetch_assets(chain, address.to_string(), providers).await } - pub async fn get_collection(&self, collection_id: NFTCollectionId) -> Result> { - let provider = self.get_provider_for_chain(collection_id.chain)?; - provider.get_collection(collection_id).await + pub async fn get_collection(&self, collection_id: NFTCollectionId) -> Option { + for provider in self.providers_for_chain(collection_id.chain) { + if let Ok(collection) = provider.get_collection(collection_id.clone()).await { + return Some(collection); + } + } + None } - pub async fn get_asset(&self, asset_id: NFTAssetId) -> Result> { - let provider = self.get_provider_for_chain(asset_id.chain)?; - provider.get_asset(asset_id).await + pub async fn get_asset(&self, asset_id: NFTAssetId) -> Option { + for provider in self.providers_for_chain(asset_id.chain) { + if let Ok(asset) = provider.get_asset(asset_id.clone()).await { + return Some(asset); + } + } + None } } diff --git a/crates/nft/src/providers/magiceden/evm/provider.rs b/crates/nft/src/providers/magiceden/evm/provider.rs index a77bda6db..782d8f970 100644 --- a/crates/nft/src/providers/magiceden/evm/provider.rs +++ b/crates/nft/src/providers/magiceden/evm/provider.rs @@ -1,6 +1,6 @@ use std::error::Error; -use primitives::{Chain, NFTAsset, NFTAssetId, NFTCollection, NFTCollectionId}; +use primitives::{Chain, NFTAsset, NFTAssetId, NFTChain, NFTCollection, NFTCollectionId}; use super::client::MagicEdenEvmClient; use super::mapper::{map_asset, map_assets, map_collection}; @@ -12,8 +12,8 @@ impl NFTProvider for MagicEdenEvmClient { "MagicEdenEVM" } - fn get_chains(&self) -> Vec { - vec![Chain::SmartChain] + fn chains(&self) -> &'static [NFTChain] { + &[NFTChain::SmartChain] } async fn get_assets(&self, chain: Chain, address: String) -> Result, Box> { diff --git a/crates/nft/src/providers/magiceden/solana/provider.rs b/crates/nft/src/providers/magiceden/solana/provider.rs index 7e4991221..4df98dde2 100644 --- a/crates/nft/src/providers/magiceden/solana/provider.rs +++ b/crates/nft/src/providers/magiceden/solana/provider.rs @@ -1,6 +1,6 @@ use std::error::Error; -use primitives::{Chain, NFTAsset, NFTAssetId, NFTCollection, NFTCollectionId}; +use primitives::{Chain, NFTAsset, NFTAssetId, NFTChain, NFTCollection, NFTCollectionId}; use super::client::MagicEdenSolanaClient; use super::mapper::{map_asset, map_assets, map_collection}; @@ -12,8 +12,8 @@ impl NFTProvider for MagicEdenSolanaClient { "MagicEdenSolana" } - fn get_chains(&self) -> Vec { - vec![Chain::Solana] + fn chains(&self) -> &'static [NFTChain] { + &[NFTChain::Solana] } async fn get_assets(&self, chain: Chain, address: String) -> Result, Box> { diff --git a/crates/nft/src/providers/opensea/provider.rs b/crates/nft/src/providers/opensea/provider.rs index e87ae190d..2e254aef2 100644 --- a/crates/nft/src/providers/opensea/provider.rs +++ b/crates/nft/src/providers/opensea/provider.rs @@ -1,6 +1,6 @@ use std::error::Error; -use primitives::{Chain, NFTAsset, NFTAssetId, NFTCollection, NFTCollectionId}; +use primitives::{Chain, NFTAsset, NFTAssetId, NFTChain, NFTCollection, NFTCollectionId}; use super::mapper::{map_asset, map_assets, map_collection}; use crate::provider::NFTProvider; @@ -12,8 +12,8 @@ impl NFTProvider for OpenSeaClient { "OpenSea" } - fn get_chains(&self) -> Vec { - vec![Chain::Ethereum, Chain::Polygon] + fn chains(&self) -> &'static [NFTChain] { + &[NFTChain::Ethereum, NFTChain::Polygon] } async fn get_assets(&self, chain: Chain, address: String) -> Result, Box> { diff --git a/crates/pricer/src/price_client.rs b/crates/pricer/src/price_client.rs index d78fcc096..08a30c8e3 100644 --- a/crates/pricer/src/price_client.rs +++ b/crates/pricer/src/price_client.rs @@ -1,6 +1,6 @@ use cacher::{CacheKey, CacherClient}; use chrono::NaiveDateTime; -use primitives::{AssetMarketPrice, AssetPriceInfo, AssetPrices, FiatRate}; +use primitives::{AssetMarketPrice, AssetPriceInfo, AssetPrices, ChartTimeframe, FiatRate}; use std::error::Error; use storage::{ ChartsRepository, Database, PricesRepository, @@ -135,16 +135,12 @@ impl PriceClient { Ok(self.database.prices()?.delete_prices_updated_at_before(time)?) } - pub async fn aggregate_hourly_charts(&self) -> Result> { - Ok(self.database.charts()?.aggregate_hourly_charts()?) + pub async fn aggregate_charts(&self, timeframe: ChartTimeframe) -> Result> { + Ok(self.database.charts()?.aggregate_charts(timeframe)?) } - pub async fn aggregate_daily_charts(&self) -> Result> { - Ok(self.database.charts()?.aggregate_daily_charts()?) - } - - pub async fn cleanup_charts_data(&self) -> Result> { - Ok(self.database.charts()?.cleanup_charts_data()?) + pub async fn cleanup_charts(&self, timeframe: ChartTimeframe) -> Result> { + Ok(self.database.charts()?.cleanup_charts(timeframe)?) } pub async fn track_observed_assets(&self, asset_ids: &[String]) -> Result<(), Box> { diff --git a/crates/primitives/src/asset_details.rs b/crates/primitives/src/asset_details.rs index 3331963be..7ca8f6c3f 100644 --- a/crates/primitives/src/asset_details.rs +++ b/crates/primitives/src/asset_details.rs @@ -19,6 +19,21 @@ pub struct AssetFull { pub market: Option, } +impl AssetFull { + pub fn with_rate(self, rate: f64) -> Self { + Self { + asset: self.asset, + properties: self.properties, + score: self.score, + tags: self.tags, + links: self.links, + perpetuals: self.perpetuals, + price: self.price.map(|p| p.with_rate(rate)), + market: self.market.map(|m| m.with_rate(rate)), + } + } +} + #[typeshare(swift = "Sendable")] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/primitives/src/asset_price.rs b/crates/primitives/src/asset_price.rs index 1fe143901..0606ee5ad 100644 --- a/crates/primitives/src/asset_price.rs +++ b/crates/primitives/src/asset_price.rs @@ -44,6 +44,26 @@ pub struct AssetMarket { pub all_time_low_change_percentage: Option, } +impl AssetMarket { + pub fn with_rate(self, rate: f64) -> Self { + Self { + market_cap: self.market_cap.map(|x| x * rate), + market_cap_fdv: self.market_cap_fdv.map(|x| x * rate), + market_cap_rank: self.market_cap_rank, + total_volume: self.total_volume.map(|x| x * rate), + circulating_supply: self.circulating_supply, + total_supply: self.total_supply, + max_supply: self.max_supply, + all_time_high: self.all_time_high.map(|x| x * rate), + all_time_high_date: self.all_time_high_date, + all_time_high_change_percentage: self.all_time_high_change_percentage, + all_time_low: self.all_time_low.map(|x| x * rate), + all_time_low_date: self.all_time_low_date, + all_time_low_change_percentage: self.all_time_low_change_percentage, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[typeshare(swift = "Sendable")] #[serde(rename_all = "camelCase")] @@ -97,6 +117,12 @@ pub enum ChartPeriod { All, } +#[derive(Debug, Clone, Copy)] +pub enum ChartTimeframe { + Hourly, + Daily, +} + impl ChartPeriod { pub fn new(period: String) -> Option { match period.to_lowercase().as_str() { diff --git a/crates/primitives/src/asset_price_info.rs b/crates/primitives/src/asset_price_info.rs index 75b7274cc..a40777bf9 100644 --- a/crates/primitives/src/asset_price_info.rs +++ b/crates/primitives/src/asset_price_info.rs @@ -15,11 +15,11 @@ pub struct AssetPriceInfo { impl AssetPriceInfo { pub fn as_price_primitive(&self) -> Price { - Price::new(self.price.price, self.price.price_change_percentage_24h, self.price.updated_at) + self.price } pub fn as_price_primitive_with_rate(&self, rate: f64) -> Price { - Price::new(self.price.price * rate, self.price.price_change_percentage_24h, self.price.updated_at) + self.price.with_rate(rate) } pub fn as_asset_price_primitive(&self) -> AssetPrice { @@ -27,11 +27,12 @@ impl AssetPriceInfo { } pub fn as_asset_price_primitive_with_rate(&self, rate: f64) -> AssetPrice { + let price = self.price.with_rate(rate); AssetPrice { asset_id: self.asset_id.clone(), - price: self.price.price * rate, - price_change_percentage_24h: self.price.price_change_percentage_24h, - updated_at: self.price.updated_at, + price: price.price, + price_change_percentage_24h: price.price_change_percentage_24h, + updated_at: price.updated_at, } } diff --git a/crates/primitives/src/chain_nft.rs b/crates/primitives/src/chain_nft.rs new file mode 100644 index 000000000..e2dc4f857 --- /dev/null +++ b/crates/primitives/src/chain_nft.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator}; +use typeshare::typeshare; + +use crate::Chain; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, EnumIter, AsRefStr, EnumString, PartialEq, Eq, Hash)] +#[typeshare(swift = "Equatable, CaseIterable, Sendable")] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum NFTChain { + Ethereum, + Polygon, + Solana, + SmartChain, +} + +impl NFTChain { + pub fn all() -> Vec { + NFTChain::iter().collect() + } +} + +impl From for Chain { + fn from(chain: NFTChain) -> Self { + match chain { + NFTChain::Ethereum => Chain::Ethereum, + NFTChain::Polygon => Chain::Polygon, + NFTChain::Solana => Chain::Solana, + NFTChain::SmartChain => Chain::SmartChain, + } + } +} diff --git a/crates/primitives/src/chart.rs b/crates/primitives/src/chart.rs index 074c351f2..cf0114a24 100644 --- a/crates/primitives/src/chart.rs +++ b/crates/primitives/src/chart.rs @@ -7,7 +7,6 @@ use typeshare::typeshare; #[serde(rename_all = "camelCase")] pub struct ChartCandleStick { pub date: DateTime, - pub interval: String, pub open: f64, pub high: f64, pub low: f64, @@ -15,6 +14,15 @@ pub struct ChartCandleStick { pub volume: f64, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[typeshare(swift = "Equatable, Sendable")] +#[serde(rename_all = "camelCase")] +pub struct ChartCandleUpdate { + pub coin: String, + pub interval: String, + pub candle: ChartCandleStick, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[typeshare(swift = "Equatable, Sendable, Hashable")] #[serde(rename_all = "camelCase")] diff --git a/crates/primitives/src/config_key.rs b/crates/primitives/src/config_key.rs index bcab621d9..ba9760944 100644 --- a/crates/primitives/src/config_key.rs +++ b/crates/primitives/src/config_key.rs @@ -118,12 +118,14 @@ pub enum ConfigKey { PriceTimerTopMarketCap, PriceTimerHighMarketCap, PriceTimerLowMarketCap, + PriceTimerVeryLowMarketCap, PriceTimerFiatRates, PriceTimerChartsHourly, PriceTimerChartsDaily, PriceTimerMarkets, PriceTimerCleanOutdated, - PriceTimerCleanupCharts, + PriceTimerCleanupChartsHourly, + PriceTimerCleanupChartsDaily, PriceOutdated, // Assets @@ -142,6 +144,7 @@ pub enum ConfigKey { FiatTimerUpdateAssets, FiatTimerUpdateProviderCountries, FiatTimerUpdateBuyableAssets, + FiatTimerUpdateSellableAssets, FiatTimerUpdateTrending, // Scan @@ -171,7 +174,9 @@ pub enum ConfigKey { // Parser ParserCatchupReloadInterval, - ParserPersistInterval, + ParserMinCheckInterval, + ParserMaxCheckInterval, + ParserErrorInterval, // Price Observed (WebSocket) PriceObservedFetchInterval, @@ -280,12 +285,14 @@ impl ConfigKey { Self::PriceTimerTopMarketCap => "60s", Self::PriceTimerHighMarketCap => "3m", Self::PriceTimerLowMarketCap => "10m", + Self::PriceTimerVeryLowMarketCap => "15m", Self::PriceTimerFiatRates => "6m", Self::PriceTimerChartsHourly => "60s", Self::PriceTimerChartsDaily => "6m", Self::PriceTimerMarkets => "1h", Self::PriceTimerCleanOutdated => "1d", - Self::PriceTimerCleanupCharts => "1d", + Self::PriceTimerCleanupChartsHourly => "1d", + Self::PriceTimerCleanupChartsDaily => "1d", Self::PriceOutdated => "7d", Self::AssetsTimerUpdateExisting => "1d", Self::AssetsTimerUpdateAll => "1d", @@ -300,13 +307,14 @@ impl ConfigKey { Self::FiatTimerUpdateAssets => "1h", Self::FiatTimerUpdateProviderCountries => "1h", Self::FiatTimerUpdateBuyableAssets => "1h", + Self::FiatTimerUpdateSellableAssets => "1h", Self::FiatTimerUpdateTrending => "1h", Self::ScanTimerUpdateValidators => "1d", Self::ScanTimerUpdateValidatorsStatic => "1h", Self::RewardsTimerAbuseChecker => "60s", Self::DeviceTimerUpdater => "1d", Self::DeviceTimerInactiveObserver => "1d", - Self::VersionTimerUpdateStoreVersions => "12h", + Self::VersionTimerUpdateStoreVersions => "1h", Self::TransactionTimerUpdater => "1d", Self::SearchAssetsUpdateInterval => "30m", Self::SearchPerpetualsUpdateInterval => "30m", @@ -315,7 +323,9 @@ impl ConfigKey { Self::SearchPerpetualsLastUpdatedAt => "0", Self::SearchNftsLastUpdatedAt => "0", Self::ParserCatchupReloadInterval => "50", - Self::ParserPersistInterval => "1s", + Self::ParserMinCheckInterval => "1s", + Self::ParserMaxCheckInterval => "8s", + Self::ParserErrorInterval => "30s", Self::PriceObservedFetchInterval => "30s", Self::PriceObservedMaxAssets => "100", Self::PriceObservedMinObservers => "2", diff --git a/crates/primitives/src/consumer_status.rs b/crates/primitives/src/consumer_status.rs deleted file mode 100644 index 3020feaf0..000000000 --- a/crates/primitives/src/consumer_status.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ConsumerStatus { - pub total_processed: u64, - pub total_errors: u64, - pub last_success: Option, - pub last_result: Option, - pub avg_duration: u64, - pub errors: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConsumerError { - pub message: String, - pub count: u64, - pub timestamp: u64, -} diff --git a/crates/primitives/src/delegation.rs b/crates/primitives/src/delegation.rs index e6a3c2357..410a8212d 100644 --- a/crates/primitives/src/delegation.rs +++ b/crates/primitives/src/delegation.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use strum::{AsRefStr, Display, EnumString}; use typeshare::typeshare; +use crate::growth_provider::GrowthProviderType; use crate::{AssetId, Chain, Price, StakeValidator}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -45,6 +46,7 @@ pub struct DelegationValidator { pub is_active: bool, pub commission: f64, pub apr: f64, + pub provider_type: GrowthProviderType, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq)] diff --git a/crates/primitives/src/earn_action.rs b/crates/primitives/src/earn_action.rs deleted file mode 100644 index 6051fafcd..000000000 --- a/crates/primitives/src/earn_action.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; -use typeshare::typeshare; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Hashable, Sendable")] -pub enum EarnAction { - Deposit, - Withdraw, -} diff --git a/crates/primitives/src/earn_position.rs b/crates/primitives/src/earn_position.rs deleted file mode 100644 index c6f0a8f55..000000000 --- a/crates/primitives/src/earn_position.rs +++ /dev/null @@ -1,21 +0,0 @@ -use num_bigint::BigInt; -use serde::{Deserialize, Serialize}; -use typeshare::typeshare; - -use crate::AssetId; -use crate::earn_provider::EarnProvider; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Hashable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct EarnPosition { - pub asset_id: AssetId, - pub provider: EarnProvider, - pub vault_token_address: String, - pub asset_token_address: String, - pub vault_balance_value: BigInt, - pub asset_balance_value: BigInt, - pub balance: String, - pub rewards: Option, - pub apy: Option, -} diff --git a/crates/primitives/src/earn_type.rs b/crates/primitives/src/earn_type.rs new file mode 100644 index 000000000..9a9129a63 --- /dev/null +++ b/crates/primitives/src/earn_type.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::{Chain, Delegation, DelegationValidator, swap::ApprovalData}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +pub enum EarnType { + Deposit(DelegationValidator), + Withdraw(Delegation), +} + +impl EarnType { + pub fn validator(&self) -> &DelegationValidator { + match self { + EarnType::Deposit(validator) => validator, + EarnType::Withdraw(delegation) => &delegation.validator, + } + } + + pub fn provider_id(&self) -> &str { + &self.validator().id + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Sendable")] +#[serde(rename_all = "camelCase")] +pub struct EarnTransaction { + pub chain: Chain, + pub from: String, + pub to: String, + pub data: String, + pub value: Option, + pub approval: Option, +} diff --git a/crates/primitives/src/earn_provider.rs b/crates/primitives/src/growth_provider.rs similarity index 52% rename from crates/primitives/src/earn_provider.rs rename to crates/primitives/src/growth_provider.rs index 51e3ac7d2..5ec600bf0 100644 --- a/crates/primitives/src/earn_provider.rs +++ b/crates/primitives/src/growth_provider.rs @@ -6,6 +6,15 @@ use typeshare::typeshare; #[typeshare(swift = "Equatable, CaseIterable, Sendable")] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] -pub enum EarnProvider { +pub enum GrowthProviderType { + Stake, + Earn, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq, Eq)] +#[typeshare(swift = "Equatable, CaseIterable, Sendable")] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum YieldProvider { Yo, } diff --git a/crates/primitives/src/job_status.rs b/crates/primitives/src/job_status.rs deleted file mode 100644 index cab96c34a..000000000 --- a/crates/primitives/src/job_status.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct JobStatus { - pub interval: u64, - pub duration: u64, - pub last_success: Option, - pub last_error: Option, - pub last_error_at: Option, -} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 4d2046827..f27a9016a 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -12,6 +12,8 @@ pub use self::chain::Chain; pub mod chain_config; pub mod chain_stake; pub use self::chain_stake::StakeChain; +pub mod chain_nft; +pub use self::chain_nft::NFTChain; pub mod chain_type; pub use self::chain_type::ChainType; pub mod chain_evm; @@ -53,7 +55,7 @@ pub use self::asset_score::AssetScore; pub mod asset_type; pub use self::asset_type::{AssetSubtype, AssetType}; pub mod asset_price; -pub use self::asset_price::{AssetMarket, AssetPrice, AssetPrices, AssetPricesRequest, ChartPeriod, ChartValue, Charts}; +pub use self::asset_price::{AssetMarket, AssetPrice, AssetPrices, AssetPricesRequest, ChartPeriod, ChartTimeframe, ChartValue, Charts}; pub mod asset_price_info; pub use self::asset_price_info::AssetPriceInfo; pub mod asset_details; @@ -100,10 +102,8 @@ pub use self::recent_activity_type::RecentActivityType; pub mod transaction_direction; pub use self::transaction_direction::TransactionDirection; pub mod subscription; -pub mod support; pub mod transaction_utxo; -pub use self::subscription::{DeviceSubscription, Subscription, WalletSubscription, WalletSubscriptionChains}; -pub use self::support::{NewSupportDevice, SupportDevice, SupportDeviceRequest}; +pub use self::subscription::{DeviceSubscription, WalletSubscription, WalletSubscriptionChains}; pub use self::transaction_utxo::TransactionUtxoInput; pub mod address_formatter; pub use self::address_formatter::AddressFormatter; @@ -217,10 +217,10 @@ pub mod chart; pub use self::chart::{ChartCandleStick, ChartDateValue}; pub mod delegation; pub use self::delegation::{Delegation, DelegationBase, DelegationState, DelegationValidator}; -pub mod earn_provider; -pub use self::earn_provider::EarnProvider; -pub mod earn_position; -pub use self::earn_position::EarnPosition; +pub mod growth_provider; +pub use self::growth_provider::{GrowthProviderType, YieldProvider}; +pub mod earn_type; +pub use self::earn_type::EarnType; pub mod transaction_update; pub use self::transaction_update::{TransactionChange, TransactionMetadata, TransactionStateRequest, TransactionUpdate}; pub mod transaction_preload_input; @@ -228,15 +228,14 @@ 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, StakeType}; +pub use self::stake_type::{RedelegateData, Resource, StakeData, StakeType, TronStakeData, TronUnfreeze, TronVote}; pub mod transaction_load_metadata; pub use self::transaction_load_metadata::{HyperliquidOrder, TransactionLoadMetadata}; 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 earn_action; -pub use self::earn_action::EarnAction; +pub use self::earn_type::EarnTransaction; pub mod earn_data; pub use self::earn_data::EarnData; pub mod transaction_data_output; @@ -266,12 +265,8 @@ pub mod notification; pub use self::notification::InAppNotification; pub mod ip_usage_type; pub use self::ip_usage_type::IpUsageType; -pub mod job_status; -pub use self::job_status::JobStatus; -pub mod consumer_status; -pub use self::consumer_status::{ConsumerError, ConsumerStatus}; -pub mod parser_status; -pub use self::parser_status::{ParserError, ParserStatus}; +pub mod metrics; +pub use self::metrics::{ConsumerStatus, JobStatus, ParserStatus, ReportedError}; #[cfg(any(test, feature = "testkit"))] pub mod testkit; diff --git a/crates/primitives/src/metrics.rs b/crates/primitives/src/metrics.rs new file mode 100644 index 000000000..c03692095 --- /dev/null +++ b/crates/primitives/src/metrics.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReportedError { + pub message: String, + pub count: u64, + pub timestamp: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ParserStatus { + pub errors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ConsumerStatus { + pub total_processed: u64, + pub total_errors: u64, + pub last_success: Option, + pub last_result: Option, + pub avg_duration: u64, + pub errors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct JobStatus { + pub interval: u64, + pub duration: u64, + pub last_success: Option, + pub last_error: Option, + pub last_error_at: Option, + pub error_count: u64, +} diff --git a/crates/primitives/src/parser_status.rs b/crates/primitives/src/parser_status.rs deleted file mode 100644 index 8899c4ff3..000000000 --- a/crates/primitives/src/parser_status.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ParserStatus { - pub errors: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ParserError { - pub message: String, - pub count: u64, - pub timestamp: u64, -} diff --git a/crates/primitives/src/price.rs b/crates/primitives/src/price.rs index 26389526d..fb5c781b2 100644 --- a/crates/primitives/src/price.rs +++ b/crates/primitives/src/price.rs @@ -22,15 +22,12 @@ impl Price { } } - pub fn new_with_rate(&self, base_rate: f64, rate: f64) -> Self { - let rate_multiplier = rate * base_rate; - let price_value = self.price * rate_multiplier; + pub fn with_rate(self, rate: f64) -> Self { + Price::new(self.price * rate, self.price_change_percentage_24h, self.updated_at) + } - Price { - price: price_value, - price_change_percentage_24h: self.price_change_percentage_24h, - updated_at: self.updated_at, - } + pub fn new_with_rate(&self, base_rate: f64, rate: f64) -> Self { + self.with_rate(rate * base_rate) } } diff --git a/crates/primitives/src/push_notification.rs b/crates/primitives/src/push_notification.rs index fa78a1d0d..ddd80a68c 100644 --- a/crates/primitives/src/push_notification.rs +++ b/crates/primitives/src/push_notification.rs @@ -45,7 +45,6 @@ pub struct PushNotificationPayloadType { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PushNotificationTransaction { - pub wallet_index: Option, pub wallet_id: String, pub asset_id: String, #[typeshare(skip)] diff --git a/crates/primitives/src/stake_type.rs b/crates/primitives/src/stake_type.rs index 065f1d967..e2369d1a5 100644 --- a/crates/primitives/src/stake_type.rs +++ b/crates/primitives/src/stake_type.rs @@ -1,4 +1,4 @@ -use crate::{Delegation, DelegationValidator}; +use crate::{Delegation, DelegationValidator, UInt64}; use serde::{Deserialize, Serialize}; use strum::{AsRefStr, EnumString}; use typeshare::typeshare; @@ -11,7 +11,14 @@ pub struct RedelegateData { pub to_validator: DelegationValidator, } -#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, EnumString)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Sendable, Hashable")] +pub struct StakeData { + pub data: Option, + pub to: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, AsRefStr, EnumString)] #[typeshare(swift = "Equatable, Sendable, Hashable")] #[serde(rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] @@ -47,3 +54,25 @@ pub enum StakeType { Withdraw(Delegation), Freeze(FreezeData), } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Sendable, Hashable")] +pub struct TronVote { + pub validator: String, + pub count: UInt64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Sendable, Hashable")] +pub struct TronUnfreeze { + pub resource: Resource, + pub amount: UInt64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +#[typeshare(swift = "Equatable, Sendable, Hashable")] +pub enum TronStakeData { + Votes(Vec), + Unfreeze(Vec), +} diff --git a/crates/primitives/src/stream.rs b/crates/primitives/src/stream.rs index 0c422f025..f2477ce54 100644 --- a/crates/primitives/src/stream.rs +++ b/crates/primitives/src/stream.rs @@ -15,6 +15,7 @@ pub enum StreamEvent { Nft(StreamNftUpdate), Perpetual(StreamPerpetualUpdate), InAppNotification(StreamNotificationlUpdate), + NewAssets(StreamNewAssetsUpdate), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -31,6 +32,8 @@ pub enum StreamMessage { SubscribePrices(StreamMessagePrices), UnsubscribePrices(StreamMessagePrices), AddPrices(StreamMessagePrices), + SubscribeRealtimePrices(StreamMessagePrices), + UnsubscribeRealtimePrices(StreamMessagePrices), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -77,3 +80,11 @@ pub struct StreamNotificationlUpdate { pub wallet_id: WalletId, pub notification: InAppNotification, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare(swift = "Sendable")] +pub struct StreamNewAssetsUpdate { + pub wallet_id: WalletId, + pub assets: Vec, +} diff --git a/crates/primitives/src/subscription.rs b/crates/primitives/src/subscription.rs index 2dc89e024..d576bc032 100644 --- a/crates/primitives/src/subscription.rs +++ b/crates/primitives/src/subscription.rs @@ -7,13 +7,6 @@ use crate::device::Device; use crate::wallet::WalletSource; use crate::wallet_id::WalletId; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Subscription { - pub wallet_index: i32, - pub chain: Chain, - pub address: String, -} - #[derive(Clone, Debug, Serialize, Deserialize)] #[typeshare(swift = "Equatable, Hashable, Sendable")] #[serde(rename_all = "camelCase")] @@ -37,5 +30,7 @@ pub struct WalletSubscriptionChains { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DeviceSubscription { pub device: Device, - pub subscription: Subscription, + pub wallet_id: WalletId, + pub chain: Chain, + pub address: String, } diff --git a/crates/primitives/src/support.rs b/crates/primitives/src/support.rs deleted file mode 100644 index 731ad6dcb..000000000 --- a/crates/primitives/src/support.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; -use typeshare::typeshare; - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[typeshare(swift = "Equatable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct NewSupportDevice { - pub support_device_id: String, - pub device_id: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[typeshare(swift = "Equatable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct SupportDeviceRequest { - pub support_device_id: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[typeshare(swift = "Equatable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct SupportDevice { - pub support_device_id: String, - pub unread: i32, -} diff --git a/crates/primitives/src/testkit/delegation_mock.rs b/crates/primitives/src/testkit/delegation_mock.rs index 263a59666..a59f29330 100644 --- a/crates/primitives/src/testkit/delegation_mock.rs +++ b/crates/primitives/src/testkit/delegation_mock.rs @@ -1,4 +1,4 @@ -use crate::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator}; +use crate::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; use num_bigint::BigUint; impl Delegation { @@ -56,6 +56,7 @@ impl DelegationValidator { is_active: true, commission: 0.05, apr: 0.08, + provider_type: GrowthProviderType::Stake, } } } diff --git a/crates/primitives/src/transaction_input_type.rs b/crates/primitives/src/transaction_input_type.rs index 5a5b0f53e..7838b380f 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -1,5 +1,4 @@ -use crate::earn_action::EarnAction; -use crate::earn_data::EarnData; +use crate::earn_type::EarnType; use crate::stake_type::StakeType; use crate::swap::{ApprovalData, SwapData}; use crate::transaction_fee::TransactionFee; @@ -23,7 +22,7 @@ pub enum TransactionInputType { TransferNft(Asset, NFTAsset), Account(Asset, AccountDataType), Perpetual(Asset, PerpetualType), - Earn(Asset, EarnAction, EarnData), + Earn(Asset, EarnType), } impl TransactionInputType { @@ -38,7 +37,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, - TransactionInputType::Earn(asset, _, _) => asset, + TransactionInputType::Earn(asset, _) => asset, } } @@ -53,7 +52,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, - TransactionInputType::Earn(asset, _, _) => asset, + TransactionInputType::Earn(asset, _) => asset, } } @@ -78,9 +77,9 @@ impl TransactionInputType { PerpetualType::Close(_) | PerpetualType::Reduce(_) => TransactionType::PerpetualClosePosition, PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, - TransactionInputType::Earn(_, action, _) => match action { - EarnAction::Deposit => TransactionType::EarnDeposit, - EarnAction::Withdraw => TransactionType::EarnWithdraw, + TransactionInputType::Earn(_, earn_type) => match earn_type { + EarnType::Deposit(_) => TransactionType::EarnDeposit, + EarnType::Withdraw(_) => TransactionType::EarnWithdraw, }, } } diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index 613992d3a..33f77acab 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,8 +1,6 @@ -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; -use crate::{UTXO, earn_data::EarnData, solana_token_program::SolanaTokenProgramId}; +use crate::{UTXO, earn_data::EarnData, solana_token_program::SolanaTokenProgramId, stake_type::TronStakeData}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HyperliquidOrder { @@ -85,7 +83,7 @@ pub enum TransactionLoadMetadata { transaction_tree_root: String, parent_hash: String, witness_address: String, - votes: HashMap, + stake_data: TronStakeData, }, Sui { message_bytes: String, diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index b14cf759b..1928cb96e 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -63,15 +63,21 @@ pub struct Postgres { pub pool: u32, } +#[derive(Debug, Deserialize, Clone)] +#[allow(unused)] +pub struct Retry { + #[serde(deserialize_with = "duration::deserialize")] + pub delay: Duration, + #[serde(deserialize_with = "duration::deserialize")] + pub timeout: Duration, +} + #[derive(Debug, Deserialize, Clone)] #[allow(unused)] pub struct RabbitMQ { pub url: String, pub prefetch: u16, - #[serde(deserialize_with = "duration::deserialize")] - pub retry_delay: Duration, - #[serde(deserialize_with = "duration::deserialize")] - pub retry_max_delay: Duration, + pub retry: Retry, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/storage/src/database/charts.rs b/crates/storage/src/database/charts.rs index 848ef7367..c5478fb46 100644 --- a/crates/storage/src/database/charts.rs +++ b/crates/storage/src/database/charts.rs @@ -1,12 +1,13 @@ use crate::DatabaseClient; use crate::models::ChartRow; use crate::schema::charts::dsl::{charts, coin_id}; -use crate::schema::charts_daily::dsl::{charts_daily, coin_id as daily_coin_id}; -use crate::schema::charts_hourly::dsl::{charts_hourly, coin_id as hourly_coin_id}; +use crate::schema::charts_daily::dsl::{charts_daily, coin_id as daily_coin_id, created_at as daily_created_at}; +use crate::schema::charts_hourly::dsl::{charts_hourly, coin_id as hourly_coin_id, created_at as hourly_created_at}; +use chrono::Utc; use diesel::dsl::sql; use diesel::prelude::*; use diesel::result::Error; -use primitives::ChartPeriod; +use primitives::{ChartPeriod, ChartTimeframe}; pub enum ChartGranularity { Minute, @@ -21,9 +22,8 @@ pub type ChartResult = (chrono::NaiveDateTime, f64); pub(crate) trait ChartsStore { fn add_charts(&mut self, values: Vec) -> Result; fn get_charts(&mut self, target_coin_id: String, period: &ChartPeriod) -> Result, Error>; - fn aggregate_hourly_charts(&mut self) -> Result; - fn aggregate_daily_charts(&mut self) -> Result; - fn cleanup_charts_data(&mut self) -> Result; + fn aggregate_charts(&mut self, timeframe: ChartTimeframe) -> Result; + fn cleanup_charts(&mut self, timeframe: ChartTimeframe) -> Result; } impl ChartsStore for DatabaseClient { @@ -60,16 +60,25 @@ impl ChartsStore for DatabaseClient { } } - fn aggregate_hourly_charts(&mut self) -> Result { - diesel::sql_query("SELECT aggregate_hourly_charts();").execute(&mut self.connection) + fn aggregate_charts(&mut self, timeframe: ChartTimeframe) -> Result { + let query = match timeframe { + ChartTimeframe::Hourly => "SELECT aggregate_hourly_charts();", + ChartTimeframe::Daily => "SELECT aggregate_daily_charts();", + }; + diesel::sql_query(query).execute(&mut self.connection) } - fn aggregate_daily_charts(&mut self) -> Result { - diesel::sql_query("SELECT aggregate_daily_charts();").execute(&mut self.connection) - } - - fn cleanup_charts_data(&mut self) -> Result { - diesel::sql_query("SELECT cleanup_all_charts_data();").execute(&mut self.connection) + fn cleanup_charts(&mut self, timeframe: ChartTimeframe) -> Result { + match timeframe { + ChartTimeframe::Hourly => { + let cutoff = (Utc::now() - chrono::Duration::days(31)).naive_utc(); + diesel::delete(charts_hourly.filter(hourly_created_at.lt(cutoff))).execute(&mut self.connection) + } + ChartTimeframe::Daily => { + let cutoff = (Utc::now() - chrono::Duration::days(365)).naive_utc(); + diesel::delete(charts_daily.filter(daily_created_at.lt(cutoff))).execute(&mut self.connection) + } + } } } diff --git a/crates/storage/src/database/devices.rs b/crates/storage/src/database/devices.rs index c19c20ca6..e39098a15 100644 --- a/crates/storage/src/database/devices.rs +++ b/crates/storage/src/database/devices.rs @@ -1,4 +1,3 @@ -use crate::database::subscriptions::SubscriptionsStore; use crate::{DatabaseClient, models::*}; use chrono::{Duration, NaiveDateTime, Utc}; use diesel::{prelude::*, upsert::excluded}; @@ -23,7 +22,7 @@ pub trait DevicesStore { fn update_device_fields(&mut self, device_ids: Vec, updates: Vec) -> Result; fn migrate_device_id(&mut self, old_device_id: &str, new_device_id: &str) -> Result; fn delete_device(&mut self, device_id: &str) -> Result; - fn delete_devices_subscriptions_after_days(&mut self, days: i64) -> Result; + fn get_stale_device_ids(&mut self, days: i64) -> Result, diesel::result::Error>; fn get_devices_by_filter(&mut self, filters: Vec) -> Result, diesel::result::Error>; } @@ -92,12 +91,10 @@ impl DevicesStore for DatabaseClient { diesel::delete(devices.filter(device_id.eq(_device_id))).execute(&mut self.connection) } - // Delete subscriptions for inactive devices - fn delete_devices_subscriptions_after_days(&mut self, days: i64) -> Result { + fn get_stale_device_ids(&mut self, days: i64) -> Result, diesel::result::Error> { use crate::schema::devices::dsl::*; let cutoff_date = Utc::now() - Duration::days(days); - let device_ids: Vec = devices.filter(updated_at.lt(cutoff_date.naive_utc())).select(id).load(&mut self.connection)?; - SubscriptionsStore::delete_subscriptions_for_device_ids(self, device_ids) + devices.filter(updated_at.lt(cutoff_date.naive_utc())).select(id).load(&mut self.connection) } fn get_devices_by_filter(&mut self, filters: Vec) -> Result, diesel::result::Error> { diff --git a/crates/storage/src/database/mod.rs b/crates/storage/src/database/mod.rs index 92e756b73..e1a6f42bf 100644 --- a/crates/storage/src/database/mod.rs +++ b/crates/storage/src/database/mod.rs @@ -21,8 +21,6 @@ pub mod releases; pub mod rewards; pub mod rewards_redemptions; pub mod scan_addresses; -pub mod subscriptions; -pub mod support; pub mod tag; pub mod transactions; pub mod usernames; @@ -43,8 +41,7 @@ use crate::repositories::{ notifications_repository::NotificationsRepository, parser_state_repository::ParserStateRepository, perpetuals_repository::PerpetualsRepository, price_alerts_repository::PriceAlertsRepository, prices_dex_repository::PricesDexRepository, prices_repository::PricesRepository, releases_repository::ReleasesRepository, rewards_redemptions_repository::RewardsRedemptionsRepository, rewards_repository::RewardsRepository, scan_addresses_repository::ScanAddressesRepository, - subscriptions_repository::SubscriptionsRepository, support_repository::SupportRepository, tag_repository::TagRepository, transactions_repository::TransactionsRepository, - wallets_repository::WalletsRepository, + tag_repository::TagRepository, transactions_repository::TransactionsRepository, wallets_repository::WalletsRepository, }; pub fn create_pool(database_url: &str, pool_size: u32) -> PgPool { @@ -149,14 +146,6 @@ impl DatabaseClient { self } - pub fn subscriptions(&mut self) -> &mut dyn SubscriptionsRepository { - self - } - - pub fn support(&mut self) -> &mut dyn SupportRepository { - self - } - pub fn tag(&mut self) -> &mut dyn TagRepository { self } diff --git a/crates/storage/src/database/subscriptions.rs b/crates/storage/src/database/subscriptions.rs deleted file mode 100644 index d5aeeb4d5..000000000 --- a/crates/storage/src/database/subscriptions.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::schema::devices; -use crate::{DatabaseClient, models::*}; -use chrono::NaiveDateTime; -use diesel::prelude::*; -use primitives::Chain; - -pub(crate) trait SubscriptionsStore { - fn get_subscriptions_by_device_id(&mut self, device_id: &str, wallet_index: Option) -> Result, diesel::result::Error>; - fn get_subscriptions(&mut self, chain: Chain, addresses: Vec) -> Result, diesel::result::Error>; - fn get_subscriptions_by_address(&mut self, address: &str) -> Result, diesel::result::Error>; - fn add_subscriptions(&mut self, values: Vec) -> Result; - fn delete_subscriptions(&mut self, values: Vec) -> Result; - fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result; - fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, diesel::result::Error>; - fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result; - fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result; - fn get_first_subscription_date(&mut self, addresses: Vec) -> Result, diesel::result::Error>; - fn get_device_addresses(&mut self, device_id: i32, chain: &str) -> Result, diesel::result::Error>; -} - -impl SubscriptionsStore for DatabaseClient { - fn get_subscriptions_by_device_id(&mut self, _device_id: &str, _wallet_index: Option) -> Result, diesel::result::Error> { - use crate::schema::subscriptions::dsl::*; - - let mut query = subscriptions.inner_join(devices::table).filter(devices::device_id.eq(_device_id)).into_boxed(); - - if let Some(index) = _wallet_index { - query = query.filter(wallet_index.eq(index)); - } - - query.select(SubscriptionRow::as_select()).load(&mut self.connection) - } - - fn delete_subscriptions(&mut self, values: Vec) -> Result { - use crate::schema::subscriptions::dsl::*; - let mut result = 0; - for subscription in values { - result += diesel::delete( - subscriptions - .filter(device_id.eq(subscription.device_id)) - .filter(chain.eq(subscription.chain)) - .filter(address.eq(subscription.address)), - ) - .execute(&mut self.connection)?; - } - Ok(result) - } - - fn get_subscriptions(&mut self, _chain: Chain, addresses: Vec) -> Result, diesel::result::Error> { - use crate::schema::subscriptions::dsl::*; - use crate::schema::subscriptions_addresses_exclude; - - subscriptions - .inner_join(devices::table) - .filter(chain.eq(_chain.as_ref())) - .filter(address.eq_any(addresses)) - .filter(diesel::dsl::not(diesel::dsl::exists( - subscriptions_addresses_exclude::table.filter(subscriptions_addresses_exclude::address.eq(address)), - ))) - .distinct_on((device_id, chain, address)) - .select((SubscriptionRow::as_select(), crate::models::DeviceRow::as_select())) - .load(&mut self.connection) - } - - fn get_subscriptions_by_address(&mut self, _address: &str) -> Result, diesel::result::Error> { - use crate::schema::subscriptions::dsl::*; - - subscriptions - .inner_join(devices::table) - .filter(address.eq(_address)) - .select(crate::models::DeviceRow::as_select()) - .distinct() - .load(&mut self.connection) - } - - fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, diesel::result::Error> { - use crate::schema::subscriptions_addresses_exclude::dsl::*; - subscriptions_addresses_exclude.filter(address.eq_any(addresses)).select(address).load(&mut self.connection) - } - - fn add_subscriptions(&mut self, values: Vec) -> Result { - use crate::schema::subscriptions::dsl::*; - diesel::insert_into(subscriptions).values(&values).on_conflict_do_nothing().execute(&mut self.connection) - } - - fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result { - use crate::schema::subscriptions_addresses_exclude::dsl::*; - diesel::insert_into(subscriptions_addresses_exclude) - .values(values) - .on_conflict_do_nothing() - .execute(&mut self.connection) - } - - fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result { - use crate::schema::subscriptions::dsl::*; - diesel::delete(subscriptions.filter(device_id.eq_any(device_ids))).execute(&mut self.connection) - } - - fn get_subscription_address_exists(&mut self, _chain: Chain, _address: &str) -> Result { - use crate::schema::subscriptions::dsl::*; - use diesel::dsl::exists; - use diesel::select; - - select(exists(subscriptions.filter(chain.eq(_chain.as_ref())).filter(address.eq(_address)))).get_result(&mut self.connection) - } - - fn get_first_subscription_date(&mut self, addresses: Vec) -> Result, diesel::result::Error> { - use crate::schema::subscriptions::dsl::*; - subscriptions - .filter(address.eq_any(addresses)) - .select(created_at) - .order(created_at.asc()) - .first(&mut self.connection) - .optional() - } - - fn get_device_addresses(&mut self, _device_id: i32, _chain: &str) -> Result, diesel::result::Error> { - use crate::schema::subscriptions::dsl::*; - subscriptions - .filter(device_id.eq(_device_id)) - .filter(chain.eq(_chain)) - .select(address) - .load(&mut self.connection) - } -} diff --git a/crates/storage/src/database/support.rs b/crates/storage/src/database/support.rs deleted file mode 100644 index 54dfe932e..000000000 --- a/crates/storage/src/database/support.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{DatabaseClient, models::*}; -use diesel::prelude::*; - -pub trait SupportStore { - fn add_support_device(&mut self, value: SupportRow) -> Result; - fn get_support_device(&mut self, support_id_param: &str) -> Result; - fn get_support(&mut self, support_id_param: &str) -> Result; - fn support_update_unread(&mut self, support_id_param: &str, unread_value: i32) -> Result; -} - -impl SupportStore for DatabaseClient { - fn add_support_device(&mut self, value: SupportRow) -> Result { - use crate::schema::support::dsl::*; - - diesel::insert_into(support) - .values(&value) - .on_conflict(support_id) - .do_update() - .set(device_id.eq(value.device_id)) - .returning(SupportRow::as_returning()) - .get_result(&mut self.connection) - } - - fn get_support_device(&mut self, support_device_id: &str) -> Result { - use crate::schema::{devices, support}; - support::table - .inner_join(devices::table) - .filter(support::support_id.eq(support_device_id)) - .select(DeviceRow::as_select()) - .first(&mut self.connection) - } - - fn get_support(&mut self, support_device_id: &str) -> Result { - use crate::schema::support::dsl::*; - support.filter(support_id.eq(support_device_id)).select(SupportRow::as_select()).first(&mut self.connection) - } - - fn support_update_unread(&mut self, support_device_id: &str, unread_value: i32) -> Result { - use crate::schema::support::dsl::*; - diesel::update(support.filter(support_id.eq(support_device_id))) - .set(unread.eq(unread_value)) - .returning(SupportRow::as_returning()) - .get_result(&mut self.connection) - } -} diff --git a/crates/storage/src/database/wallets.rs b/crates/storage/src/database/wallets.rs index 6299077f9..ab9001f44 100644 --- a/crates/storage/src/database/wallets.rs +++ b/crates/storage/src/database/wallets.rs @@ -1,7 +1,8 @@ -use crate::models::{DeviceRow, NewWalletAddressRow, NewWalletRow, NewWalletSubscriptionRow, WalletAddressRow, WalletRow, WalletSubscriptionRow}; -use crate::schema::{devices, wallets, wallets_addresses, wallets_subscriptions}; +use crate::models::{DeviceRow, NewWalletAddressRow, NewWalletRow, NewWalletSubscriptionRow, SubscriptionAddressExcludeRow, WalletAddressRow, WalletRow, WalletSubscriptionRow}; +use crate::schema::{devices, subscriptions_addresses_exclude, wallets, wallets_addresses, wallets_subscriptions}; use crate::sql_types::ChainRow; use crate::{DatabaseClient, DatabaseError}; +use chrono::NaiveDateTime; use diesel::prelude::*; use primitives::Chain; @@ -23,6 +24,18 @@ pub trait WalletsStore { fn delete_subscriptions(&mut self, device_id: i32, wallet_id: i32, chain: ChainRow, address_ids: Vec) -> Result; fn delete_wallet_subscriptions(&mut self, device_id: i32, wallet_ids: Vec) -> Result; fn delete_wallet_chains(&mut self, device_id: i32, wallet_id: i32, chains: Vec) -> Result; + fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result; + + fn get_subscriptions_by_chain_addresses( + &mut self, + chain: Chain, + addresses: Vec, + ) -> Result, DatabaseError>; + fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result; + fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result; + fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError>; + fn get_device_addresses(&mut self, device_id: i32, chain: ChainRow) -> Result, DatabaseError>; + fn get_first_subscription_date(&mut self, addresses: Vec) -> Result, DatabaseError>; } impl WalletsStore for DatabaseClient { @@ -179,4 +192,94 @@ impl WalletsStore for DatabaseClient { Ok(count) } + + fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result { + let count = diesel::delete(wallets_subscriptions::table) + .filter(wallets_subscriptions::device_id.eq_any(device_ids)) + .execute(&mut self.connection)?; + + Ok(count) + } + + fn get_subscriptions_by_chain_addresses( + &mut self, + chain: Chain, + addresses: Vec, + ) -> Result, DatabaseError> { + let chain_row = ChainRow::from(chain); + + let results = wallets_subscriptions::table + .inner_join(wallets::table) + .inner_join(wallets_addresses::table) + .inner_join(devices::table) + .filter(wallets_subscriptions::chain.eq(chain_row)) + .filter(wallets_addresses::address.eq_any(&addresses)) + .filter(diesel::dsl::not(diesel::dsl::exists( + subscriptions_addresses_exclude::table.filter(subscriptions_addresses_exclude::address.eq(wallets_addresses::address)), + ))) + .select(( + WalletRow::as_select(), + WalletSubscriptionRow::as_select(), + WalletAddressRow::as_select(), + DeviceRow::as_select(), + )) + .load(&mut self.connection)?; + + Ok(results) + } + + fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result { + let chain_row = ChainRow::from(chain); + + let exists = diesel::select(diesel::dsl::exists( + wallets_subscriptions::table + .inner_join(wallets_addresses::table) + .filter(wallets_subscriptions::chain.eq(chain_row)) + .filter(wallets_addresses::address.eq(address)), + )) + .get_result(&mut self.connection)?; + + Ok(exists) + } + + fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result { + let count = diesel::insert_into(subscriptions_addresses_exclude::table) + .values(values) + .on_conflict_do_nothing() + .execute(&mut self.connection)?; + + Ok(count) + } + + fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError> { + let results = subscriptions_addresses_exclude::table + .filter(subscriptions_addresses_exclude::address.eq_any(addresses)) + .select(subscriptions_addresses_exclude::address) + .load(&mut self.connection)?; + + Ok(results) + } + + fn get_device_addresses(&mut self, device_id: i32, chain: ChainRow) -> Result, DatabaseError> { + let results = wallets_subscriptions::table + .inner_join(wallets_addresses::table) + .filter(wallets_subscriptions::device_id.eq(device_id)) + .filter(wallets_subscriptions::chain.eq(chain)) + .select(wallets_addresses::address) + .load(&mut self.connection)?; + + Ok(results) + } + + fn get_first_subscription_date(&mut self, addresses: Vec) -> Result, DatabaseError> { + let result = wallets_subscriptions::table + .inner_join(wallets_addresses::table) + .filter(wallets_addresses::address.eq_any(addresses)) + .select(wallets_subscriptions::created_at) + .order(wallets_subscriptions::created_at.asc()) + .first(&mut self.connection) + .optional()?; + + Ok(result) + } } diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 39e16c73e..5a0e439f4 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -23,12 +23,11 @@ pub use self::models::{AssetUsageRankRow, NewNotificationRow, NewWalletRow, Rewa pub use self::repositories::{ assets_addresses_repository::AssetsAddressesRepository, assets_links_repository::AssetsLinksRepository, assets_repository::AssetsRepository, assets_usage_ranks_repository::AssetsUsageRanksRepository, chains_repository::ChainsRepository, charts_repository::ChartsRepository, config_repository::ConfigRepository, - devices_repository::DevicesRepository, fiat_repository::FiatRepository, migrations_repository::MigrationsRepository, - nft_repository::NftRepository, notifications_repository::NotificationsRepository, parser_state_repository::ParserStateRepository, perpetuals_repository::PerpetualsRepository, + devices_repository::DevicesRepository, fiat_repository::FiatRepository, migrations_repository::MigrationsRepository, nft_repository::NftRepository, + notifications_repository::NotificationsRepository, parser_state_repository::ParserStateRepository, perpetuals_repository::PerpetualsRepository, price_alerts_repository::PriceAlertsRepository, prices_dex_repository::PricesDexRepository, prices_repository::PricesRepository, releases_repository::ReleasesRepository, rewards_redemptions_repository::RewardsRedemptionsRepository, rewards_repository::RewardsRepository, risk_signals_repository::RiskSignalsRepository, - scan_addresses_repository::ScanAddressesRepository, subscriptions_repository::SubscriptionsRepository, support_repository::SupportRepository, tag_repository::TagRepository, - transactions_repository::TransactionsRepository, wallets_repository::WalletsRepository, + scan_addresses_repository::ScanAddressesRepository, tag_repository::TagRepository, transactions_repository::TransactionsRepository, wallets_repository::WalletsRepository, }; pub use self::sql_types::{NotificationType, WalletSource, WalletType}; pub use diesel::OptionalExtension; @@ -127,14 +126,6 @@ impl Database { self.client() } - pub fn subscriptions(&self) -> Result> { - self.client() - } - - pub fn support(&self) -> Result> { - self.client() - } - pub fn tag(&self) -> Result> { self.client() } diff --git a/crates/storage/src/migrations/2023-07-29-000000_charts/down.sql b/crates/storage/src/migrations/2023-07-29-000000_charts/down.sql index fb9828128..76607614c 100644 --- a/crates/storage/src/migrations/2023-07-29-000000_charts/down.sql +++ b/crates/storage/src/migrations/2023-07-29-000000_charts/down.sql @@ -1,7 +1,6 @@ -- Drop functions DROP FUNCTION IF EXISTS aggregate_hourly_charts(); DROP FUNCTION IF EXISTS aggregate_daily_charts(); -DROP FUNCTION IF EXISTS cleanup_all_charts_data(); -- Drop tables DROP TABLE IF EXISTS charts_daily; diff --git a/crates/storage/src/migrations/2023-07-29-000000_charts/up.sql b/crates/storage/src/migrations/2023-07-29-000000_charts/up.sql index 84e408d58..302c786ee 100644 --- a/crates/storage/src/migrations/2023-07-29-000000_charts/up.sql +++ b/crates/storage/src/migrations/2023-07-29-000000_charts/up.sql @@ -55,14 +55,6 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION cleanup_all_charts_data() RETURNS VOID AS $$ -BEGIN - DELETE FROM charts WHERE created_at < NOW() - INTERVAL '8 days'; - DELETE FROM charts_hourly WHERE created_at < NOW() - INTERVAL '31 days'; - DELETE FROM charts_daily WHERE created_at < NOW() - INTERVAL '365 days'; -END; -$$ LANGUAGE plpgsql; - ALTER TABLE charts SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 1000); ALTER TABLE charts_hourly SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 500); ALTER TABLE charts_daily SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 500); \ No newline at end of file diff --git a/crates/storage/src/migrations/2023-09-04-220616_subscriptions/down.sql b/crates/storage/src/migrations/2023-09-04-220616_subscriptions/down.sql index 9f510384e..85675db84 100644 --- a/crates/storage/src/migrations/2023-09-04-220616_subscriptions/down.sql +++ b/crates/storage/src/migrations/2023-09-04-220616_subscriptions/down.sql @@ -1,2 +1 @@ DROP TABLE IF EXISTS subscriptions_addresses_exclude; -DROP TABLE IF EXISTS subscriptions; diff --git a/crates/storage/src/migrations/2023-09-04-220616_subscriptions/up.sql b/crates/storage/src/migrations/2023-09-04-220616_subscriptions/up.sql index 91861c5bc..036e5117d 100644 --- a/crates/storage/src/migrations/2023-09-04-220616_subscriptions/up.sql +++ b/crates/storage/src/migrations/2023-09-04-220616_subscriptions/up.sql @@ -1,16 +1,3 @@ -CREATE TABLE subscriptions ( - id SERIAL PRIMARY KEY, - device_id INTEGER NOT NULL REFERENCES devices (id) ON DELETE CASCADE, - chain VARCHAR NOT NULL REFERENCES chains (id) ON DELETE CASCADE, - address VARCHAR(256) NOT NULL, - created_at timestamp NOT NULL DEFAULT current_timestamp, - wallet_index INTEGER NOT NULL, - UNIQUE(device_id, wallet_index, chain, address) -); - -CREATE INDEX subscriptions_address_idx ON subscriptions (address DESC); -CREATE INDEX subscriptions_chain_idx ON subscriptions (chain DESC); - CREATE TABLE subscriptions_addresses_exclude ( address VARCHAR(128) PRIMARY KEY NOT NULL, chain VARCHAR(32) NOT NULL REFERENCES chains (id) ON DELETE CASCADE, diff --git a/crates/storage/src/migrations/2025-09-15-170321_support/down.sql b/crates/storage/src/migrations/2025-09-15-170321_support/down.sql deleted file mode 100644 index ef99da99a..000000000 --- a/crates/storage/src/migrations/2025-09-15-170321_support/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE support; diff --git a/crates/storage/src/migrations/2025-09-15-170321_support/up.sql b/crates/storage/src/migrations/2025-09-15-170321_support/up.sql deleted file mode 100644 index 819549e4c..000000000 --- a/crates/storage/src/migrations/2025-09-15-170321_support/up.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE support ( - id SERIAL PRIMARY KEY, - support_id VARCHAR(32) NOT NULL UNIQUE, - device_id INTEGER NOT NULL REFERENCES devices (id) ON DELETE CASCADE, - unread INTEGER NOT NULL DEFAULT 0, - - updated_at timestamp NOT NULL default current_timestamp, - created_at timestamp NOT NULL default current_timestamp, - - UNIQUE (device_id, support_id) -); - -SELECT diesel_manage_updated_at('support'); diff --git a/crates/storage/src/migrations/2025-12-09-120000_add_wallets/down.sql b/crates/storage/src/migrations/2025-12-09-120000_add_wallets/down.sql index 262352f93..d3bcb7e5b 100644 --- a/crates/storage/src/migrations/2025-12-09-120000_add_wallets/down.sql +++ b/crates/storage/src/migrations/2025-12-09-120000_add_wallets/down.sql @@ -1,3 +1,4 @@ +DROP INDEX IF EXISTS wallets_subscriptions_device_chain_idx; DROP INDEX IF EXISTS wallets_subscriptions_chain_wallet_address_id_idx; DROP INDEX IF EXISTS wallets_subscriptions_wallet_address_id_idx; DROP INDEX IF EXISTS wallets_subscriptions_device_id_idx; diff --git a/crates/storage/src/migrations/2025-12-09-120000_add_wallets/up.sql b/crates/storage/src/migrations/2025-12-09-120000_add_wallets/up.sql index 40e2c8b61..f1899a640 100644 --- a/crates/storage/src/migrations/2025-12-09-120000_add_wallets/up.sql +++ b/crates/storage/src/migrations/2025-12-09-120000_add_wallets/up.sql @@ -28,3 +28,4 @@ CREATE INDEX wallets_subscriptions_wallet_id_idx ON wallets_subscriptions (walle CREATE INDEX wallets_subscriptions_device_id_idx ON wallets_subscriptions (device_id); CREATE INDEX wallets_subscriptions_address_id_idx ON wallets_subscriptions (address_id); CREATE INDEX wallets_subscriptions_chain_address_id_idx ON wallets_subscriptions (chain, address_id); +CREATE INDEX wallets_subscriptions_device_chain_idx ON wallets_subscriptions (device_id, chain); diff --git a/crates/storage/src/migrations/2026-02-07-120000_drop_subscriptions/up.sql b/crates/storage/src/migrations/2026-02-07-120000_drop_subscriptions/up.sql new file mode 100644 index 000000000..fa93c5b71 --- /dev/null +++ b/crates/storage/src/migrations/2026-02-07-120000_drop_subscriptions/up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS subscriptions; diff --git a/crates/storage/src/models/mod.rs b/crates/storage/src/models/mod.rs index b6a2211b2..c8a1256e0 100644 --- a/crates/storage/src/models/mod.rs +++ b/crates/storage/src/models/mod.rs @@ -20,8 +20,7 @@ pub mod price_dex; pub mod release; pub mod reward; pub mod scan_addresses; -pub mod subscription; -pub mod support; +pub mod subscription_address_exclude; pub mod tag; pub mod transaction; pub mod transaction_addresses; @@ -55,8 +54,7 @@ pub use self::reward::{ RewardRedemptionOptionRow, RewardRedemptionRow, RewardReferralRow, RewardsRow, RiskSignalRow, }; pub use self::scan_addresses::{NewScanAddressRow, ScanAddressRow}; -pub use self::subscription::{SubscriptionAddressExcludeRow, SubscriptionRow}; -pub use self::support::SupportRow; +pub use self::subscription_address_exclude::SubscriptionAddressExcludeRow; pub use self::tag::{AssetTagRow, TagRow}; pub use self::transaction::{NewTransactionRow, TransactionRow}; pub use self::transaction_addresses::{AddressChainIdResultRow, NewTransactionAddressesRow, TransactionAddressesRow}; diff --git a/crates/storage/src/models/subscription.rs b/crates/storage/src/models/subscription.rs deleted file mode 100644 index 131dd3f50..000000000 --- a/crates/storage/src/models/subscription.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::str::FromStr; - -use diesel::prelude::*; -use primitives::{Chain, ChainAddress, Subscription}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Queryable, Selectable, Serialize, Deserialize, Insertable, AsChangeset, Clone)] -#[diesel(table_name = crate::schema::subscriptions)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct SubscriptionRow { - pub device_id: i32, - pub wallet_index: i32, - pub chain: String, - pub address: String, -} - -#[derive(Debug, Queryable, Selectable, Serialize, Deserialize, Insertable, AsChangeset, Clone)] -#[diesel(table_name = crate::schema::subscriptions_addresses_exclude)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct SubscriptionAddressExcludeRow { - pub address: String, - pub chain: String, -} - -impl SubscriptionRow { - pub fn as_primitive(&self) -> Subscription { - Subscription { - wallet_index: self.wallet_index, - chain: Chain::from_str(self.chain.as_ref()).unwrap(), - address: self.address.clone(), - } - } - - pub fn as_chain_address(&self) -> ChainAddress { - ChainAddress { - chain: Chain::from_str(self.chain.as_ref()).unwrap(), - address: self.address.clone(), - } - } - - pub fn from_primitive(subscription: Subscription, device_id: i32, wallet_index: i32) -> Self { - Self { - device_id, - wallet_index, - chain: subscription.chain.as_ref().to_string(), - address: subscription.address.to_string(), - } - } -} diff --git a/crates/storage/src/models/subscription_address_exclude.rs b/crates/storage/src/models/subscription_address_exclude.rs new file mode 100644 index 000000000..b78a2f76c --- /dev/null +++ b/crates/storage/src/models/subscription_address_exclude.rs @@ -0,0 +1,10 @@ +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Queryable, Selectable, Serialize, Deserialize, Insertable, AsChangeset, Clone)] +#[diesel(table_name = crate::schema::subscriptions_addresses_exclude)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct SubscriptionAddressExcludeRow { + pub address: String, + pub chain: String, +} diff --git a/crates/storage/src/models/support.rs b/crates/storage/src/models/support.rs deleted file mode 100644 index 400409241..000000000 --- a/crates/storage/src/models/support.rs +++ /dev/null @@ -1,21 +0,0 @@ -use diesel::prelude::*; -use primitives::SupportDevice; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Queryable, Selectable, Serialize, Deserialize, Insertable, AsChangeset, Clone)] -#[diesel(table_name = crate::schema::support)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct SupportRow { - pub support_id: String, - pub device_id: i32, - pub unread: i32, -} - -impl SupportRow { - pub fn as_primitive(&self) -> SupportDevice { - SupportDevice { - support_device_id: self.support_id.clone(), - unread: self.unread, - } - } -} diff --git a/crates/storage/src/repositories/charts_repository.rs b/crates/storage/src/repositories/charts_repository.rs index 415195164..b8b29d795 100644 --- a/crates/storage/src/repositories/charts_repository.rs +++ b/crates/storage/src/repositories/charts_repository.rs @@ -2,14 +2,13 @@ use crate::DatabaseError; use crate::DatabaseClient; use crate::database::charts::{ChartResult, ChartsStore}; -use primitives::ChartPeriod; +use primitives::{ChartPeriod, ChartTimeframe}; pub trait ChartsRepository { fn add_charts(&mut self, values: Vec) -> Result; fn get_charts(&mut self, target_coin_id: String, period: &ChartPeriod) -> Result, DatabaseError>; - fn aggregate_hourly_charts(&mut self) -> Result; - fn aggregate_daily_charts(&mut self) -> Result; - fn cleanup_charts_data(&mut self) -> Result; + fn aggregate_charts(&mut self, timeframe: ChartTimeframe) -> Result; + fn cleanup_charts(&mut self, timeframe: ChartTimeframe) -> Result; } impl ChartsRepository for DatabaseClient { @@ -21,15 +20,11 @@ impl ChartsRepository for DatabaseClient { Ok(ChartsStore::get_charts(self, target_coin_id, period)?) } - fn aggregate_hourly_charts(&mut self) -> Result { - Ok(ChartsStore::aggregate_hourly_charts(self)?) + fn aggregate_charts(&mut self, timeframe: ChartTimeframe) -> Result { + Ok(ChartsStore::aggregate_charts(self, timeframe)?) } - fn aggregate_daily_charts(&mut self) -> Result { - Ok(ChartsStore::aggregate_daily_charts(self)?) - } - - fn cleanup_charts_data(&mut self) -> Result { - Ok(ChartsStore::cleanup_charts_data(self)?) + fn cleanup_charts(&mut self, timeframe: ChartTimeframe) -> Result { + Ok(ChartsStore::cleanup_charts(self, timeframe)?) } } diff --git a/crates/storage/src/repositories/devices_repository.rs b/crates/storage/src/repositories/devices_repository.rs index ffeb23e0c..5fa85193e 100644 --- a/crates/storage/src/repositories/devices_repository.rs +++ b/crates/storage/src/repositories/devices_repository.rs @@ -1,4 +1,5 @@ use crate::database::devices::{DeviceFieldUpdate, DeviceFilter, DevicesStore}; +use crate::database::wallets::WalletsStore; use crate::{DatabaseClient, DatabaseError}; use primitives::Device; @@ -58,7 +59,8 @@ impl DevicesRepository for DatabaseClient { } fn delete_devices_subscriptions_after_days(&mut self, days: i64) -> Result { - Ok(DevicesStore::delete_devices_subscriptions_after_days(self, days)?) + let device_ids = DevicesStore::get_stale_device_ids(self, days)?; + WalletsStore::delete_subscriptions_for_device_ids(self, device_ids) } fn devices_inactive_days(&mut self, min_days: i64, max_days: i64, push_enabled: Option) -> Result, DatabaseError> { diff --git a/crates/storage/src/repositories/mod.rs b/crates/storage/src/repositories/mod.rs index 54951da16..dd02ccfbf 100644 --- a/crates/storage/src/repositories/mod.rs +++ b/crates/storage/src/repositories/mod.rs @@ -20,8 +20,6 @@ pub mod rewards_redemptions_repository; pub mod rewards_repository; pub mod risk_signals_repository; pub mod scan_addresses_repository; -pub mod subscriptions_repository; -pub mod support_repository; pub mod tag_repository; pub mod transactions_repository; pub mod wallets_repository; diff --git a/crates/storage/src/repositories/rewards_repository.rs b/crates/storage/src/repositories/rewards_repository.rs index 36794e911..cc54d8568 100644 --- a/crates/storage/src/repositories/rewards_repository.rs +++ b/crates/storage/src/repositories/rewards_repository.rs @@ -1,9 +1,9 @@ use crate::database::rewards::{ReferralUpdate, RewardsStore}; -use crate::database::subscriptions::SubscriptionsStore; use crate::database::usernames::{UsernameLookup, UsernamesStore}; use crate::database::wallets::WalletsStore; use crate::models::{NewRewardEventRow, NewRewardReferralRow, NewRewardsRow, NewUsernameRow, ReferralAttemptRow, RewardsRow}; use crate::repositories::rewards_redemptions_repository::RewardsRedemptionsRepository; +use crate::sql_types::ChainRow; use crate::sql_types::{RewardEventType, RewardRedemptionType, RewardStatus, UsernameStatus}; use crate::{DatabaseClient, DatabaseError, ReferralValidationError}; use chrono::Duration as ChronoDuration; @@ -217,7 +217,7 @@ impl RewardsRepository for DatabaseClient { return Err(ReferralValidationError::RewardsNotEnabled(referrer_username.to_string())); } - let device_subscriptions = SubscriptionsStore::get_device_addresses(self, device_id, Chain::Ethereum.as_ref())?; + let device_subscriptions = WalletsStore::get_device_addresses(self, device_id, ChainRow::from(Chain::Ethereum))?; for address in &device_subscriptions { let wallet_identifier = WalletId::Multicoin(address.clone()).id(); @@ -257,7 +257,7 @@ impl RewardsRepository for DatabaseClient { } fn get_first_subscription_date(&mut self, addresses: Vec) -> Result, DatabaseError> { - Ok(SubscriptionsStore::get_first_subscription_date(self, addresses)?) + WalletsStore::get_first_subscription_date(self, addresses) } fn get_wallet_id_by_username(&mut self, username: &str) -> Result { diff --git a/crates/storage/src/repositories/subscriptions_repository.rs b/crates/storage/src/repositories/subscriptions_repository.rs deleted file mode 100644 index 5874bf822..000000000 --- a/crates/storage/src/repositories/subscriptions_repository.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::DatabaseError; - -use crate::DatabaseClient; -use crate::database::subscriptions::SubscriptionsStore; -use primitives::{Chain, Device, DeviceSubscription, Subscription as PrimitiveSubscription}; - -pub trait SubscriptionsRepository { - fn get_subscriptions_by_device_id(&mut self, device_id: &str, wallet_index: Option) -> Result, DatabaseError>; - fn get_subscriptions(&mut self, chain: Chain, addresses: Vec) -> Result, DatabaseError>; - fn get_devices_by_address(&mut self, address: &str) -> Result, DatabaseError>; - fn add_subscriptions(&mut self, values: Vec, device_id: &str) -> Result; - fn delete_subscriptions(&mut self, values: Vec, device_id: &str) -> Result; - fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result; - fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError>; - fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result; - fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result; -} - -impl SubscriptionsRepository for DatabaseClient { - fn get_subscriptions_by_device_id(&mut self, device_id: &str, wallet_index: Option) -> Result, DatabaseError> { - Ok(SubscriptionsStore::get_subscriptions_by_device_id(self, device_id, wallet_index)? - .into_iter() - .map(|x| x.as_primitive()) - .collect()) - } - - fn delete_subscriptions(&mut self, values: Vec, device_id: &str) -> Result { - use crate::database::devices::DevicesStore; - let device = DevicesStore::get_device(self, device_id)?; - Ok(SubscriptionsStore::delete_subscriptions( - self, - values - .into_iter() - .map(|x| crate::models::SubscriptionRow::from_primitive(x.clone(), device.id, x.wallet_index)) - .collect(), - )?) - } - - fn get_subscriptions(&mut self, chain: Chain, addresses: Vec) -> Result, DatabaseError> { - Ok(SubscriptionsStore::get_subscriptions(self, chain, addresses)? - .into_iter() - .map(|(subscription, device)| DeviceSubscription { - device: device.as_primitive(), - subscription: subscription.as_primitive(), - }) - .collect()) - } - - fn get_devices_by_address(&mut self, address: &str) -> Result, DatabaseError> { - Ok(SubscriptionsStore::get_subscriptions_by_address(self, address)? - .into_iter() - .map(|d| d.as_primitive()) - .collect()) - } - - fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError> { - Ok(SubscriptionsStore::get_subscriptions_exclude_addresses(self, addresses)?) - } - - fn add_subscriptions(&mut self, values: Vec, device_id: &str) -> Result { - use crate::database::devices::DevicesStore; - let device = DevicesStore::get_device(self, device_id)?; - Ok(SubscriptionsStore::add_subscriptions( - self, - values - .into_iter() - .map(|x| crate::models::SubscriptionRow::from_primitive(x.clone(), device.id, x.wallet_index)) - .collect(), - )?) - } - - fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result { - Ok(SubscriptionsStore::add_subscriptions_exclude_addresses(self, values)?) - } - - fn delete_subscriptions_for_device_ids(&mut self, device_ids: Vec) -> Result { - Ok(SubscriptionsStore::delete_subscriptions_for_device_ids(self, device_ids)?) - } - - fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result { - Ok(SubscriptionsStore::get_subscription_address_exists(self, chain, address)?) - } -} diff --git a/crates/storage/src/repositories/support_repository.rs b/crates/storage/src/repositories/support_repository.rs deleted file mode 100644 index f27a16782..000000000 --- a/crates/storage/src/repositories/support_repository.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::database::devices::DevicesStore; -use crate::database::support::SupportStore; -use crate::models::SupportRow; -use crate::{DatabaseClient, DatabaseError}; -use primitives::SupportDevice; - -pub trait SupportRepository { - fn add_support_device(&mut self, support_id: &str, device_id: &str) -> Result; - fn get_support(&mut self, support_device_id: &str) -> Result; - fn support_update_unread(&mut self, support_device_id: &str, unread: i32) -> Result; -} - -impl SupportRepository for DatabaseClient { - fn add_support_device(&mut self, support_id: &str, device_id: &str) -> Result { - let device = DevicesStore::get_device(self, device_id)?; - let support = SupportStore::add_support_device( - self, - SupportRow { - support_id: support_id.to_string(), - device_id: device.id, - unread: 0, - }, - )?; - - Ok(support.as_primitive()) - } - - fn get_support(&mut self, support_device_id: &str) -> Result { - let support = SupportStore::get_support(self, support_device_id)?; - Ok(support.as_primitive()) - } - - fn support_update_unread(&mut self, support_device_id: &str, unread: i32) -> Result { - let support = SupportStore::support_update_unread(self, support_device_id, unread)?; - Ok(support.as_primitive()) - } -} diff --git a/crates/storage/src/repositories/wallets_repository.rs b/crates/storage/src/repositories/wallets_repository.rs index 6fe97f6b5..6d4d62e6c 100644 --- a/crates/storage/src/repositories/wallets_repository.rs +++ b/crates/storage/src/repositories/wallets_repository.rs @@ -1,8 +1,8 @@ use crate::database::wallets::WalletsStore; -use crate::models::{DeviceRow, NewWalletAddressRow, NewWalletRow, NewWalletSubscriptionRow, WalletAddressRow, WalletRow, WalletSubscriptionRow}; +use crate::models::{DeviceRow, NewWalletAddressRow, NewWalletRow, NewWalletSubscriptionRow, SubscriptionAddressExcludeRow, WalletAddressRow, WalletRow, WalletSubscriptionRow}; use crate::sql_types::ChainRow; use crate::{DatabaseClient, DatabaseError}; -use primitives::Chain; +use primitives::{Chain, DeviceSubscription}; use std::collections::{HashMap, HashSet}; pub trait WalletsRepository { @@ -19,6 +19,11 @@ pub trait WalletsRepository { fn delete_subscriptions(&mut self, device_id: i32, subscriptions: Vec<(i32, Chain, String)>) -> Result; fn delete_wallet_subscriptions(&mut self, device_id: i32, wallet_ids: Vec) -> Result; fn delete_wallet_chains(&mut self, device_id: i32, wallet_id: i32, chains: Vec) -> Result; + + fn get_subscriptions_by_chain_addresses(&mut self, chain: Chain, addresses: Vec) -> Result, DatabaseError>; + fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result; + fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result; + fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError>; } impl WalletsRepository for DatabaseClient { @@ -143,4 +148,28 @@ impl WalletsRepository for DatabaseClient { fn delete_wallet_chains(&mut self, device_id: i32, wallet_id: i32, chains: Vec) -> Result { WalletsStore::delete_wallet_chains(self, device_id, wallet_id, chains) } + + fn get_subscriptions_by_chain_addresses(&mut self, chain: Chain, addresses: Vec) -> Result, DatabaseError> { + Ok(WalletsStore::get_subscriptions_by_chain_addresses(self, chain, addresses)? + .into_iter() + .map(|(wallet, sub, addr, device)| DeviceSubscription { + device: device.as_primitive(), + wallet_id: wallet.wallet_id.0.clone(), + chain: sub.chain.0, + address: addr.address, + }) + .collect()) + } + + fn get_subscription_address_exists(&mut self, chain: Chain, address: &str) -> Result { + WalletsStore::get_subscription_address_exists(self, chain, address) + } + + fn add_subscriptions_exclude_addresses(&mut self, values: Vec) -> Result { + WalletsStore::add_subscriptions_exclude_addresses(self, values) + } + + fn get_subscriptions_exclude_addresses(&mut self, addresses: Vec) -> Result, DatabaseError> { + WalletsStore::get_subscriptions_exclude_addresses(self, addresses) + } } diff --git a/crates/storage/src/schema.rs b/crates/storage/src/schema.rs index 637307b4c..f2f862a43 100644 --- a/crates/storage/src/schema.rs +++ b/crates/storage/src/schema.rs @@ -811,18 +811,6 @@ diesel::table! { } } -diesel::table! { - subscriptions (id) { - id -> Int4, - device_id -> Int4, - chain -> Varchar, - #[max_length = 256] - address -> Varchar, - created_at -> Timestamp, - wallet_index -> Int4, - } -} - diesel::table! { subscriptions_addresses_exclude (address) { #[max_length = 128] @@ -836,18 +824,6 @@ diesel::table! { } } -diesel::table! { - support (id) { - id -> Int4, - #[max_length = 32] - support_id -> Varchar, - device_id -> Int4, - unread -> Int4, - updated_at -> Timestamp, - created_at -> Timestamp, - } -} - diesel::table! { tags (id) { #[max_length = 64] @@ -1009,10 +985,7 @@ diesel::joinable!(rewards_referrals -> rewards_risk_signals (risk_signal_id)); diesel::joinable!(rewards_risk_signals -> devices (device_id)); diesel::joinable!(rewards_risk_signals -> rewards (referrer_username)); diesel::joinable!(scan_addresses -> chains (chain)); -diesel::joinable!(subscriptions -> chains (chain)); -diesel::joinable!(subscriptions -> devices (device_id)); diesel::joinable!(subscriptions_addresses_exclude -> chains (chain)); -diesel::joinable!(support -> devices (device_id)); diesel::joinable!(transactions -> chains (chain)); diesel::joinable!(transactions_addresses -> assets (asset_id)); diesel::joinable!(transactions_addresses -> transactions (transaction_id)); @@ -1066,9 +1039,7 @@ diesel::allow_tables_to_appear_in_same_query!( rewards_referrals, rewards_risk_signals, scan_addresses, - subscriptions, subscriptions_addresses_exclude, - support, tags, transactions, transactions_addresses, diff --git a/crates/streamer/src/connection.rs b/crates/streamer/src/connection.rs index 3e8e38a0e..b0f05bb6d 100644 --- a/crates/streamer/src/connection.rs +++ b/crates/streamer/src/connection.rs @@ -5,14 +5,28 @@ use lapin::{Channel, Connection, ConnectionProperties}; #[derive(Clone)] pub struct StreamConnection { + url: String, + name: String, connection: Arc, } impl StreamConnection { pub async fn new(url: &str, name: impl Into) -> Result> { let name: String = name.into(); - let connection = Connection::connect(url, ConnectionProperties::default().with_connection_name(name.into())).await?; - Ok(Self { connection: Arc::new(connection) }) + let connection = Connection::connect(url, ConnectionProperties::default().with_connection_name(name.clone().into())).await?; + Ok(Self { + url: url.to_string(), + name, + connection: Arc::new(connection), + }) + } + + pub fn url(&self) -> &str { + &self.url + } + + pub fn name(&self) -> &str { + &self.name } pub async fn create_channel(&self) -> Result> { diff --git a/crates/streamer/src/consumer.rs b/crates/streamer/src/consumer.rs index b8b235800..3d3c57957 100644 --- a/crates/streamer/src/consumer.rs +++ b/crates/streamer/src/consumer.rs @@ -1,5 +1,3 @@ -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; use std::{ error::Error, @@ -26,9 +24,10 @@ enum ProcessResult { Error(Box), } +#[async_trait] pub trait ConsumerStatusReporter: Send + Sync { - fn report_success(&self, name: &str, duration: u64, result: &str) -> Pin + Send + '_>>; - fn report_error(&self, name: &str, error: &str) -> Pin + Send + '_>>; + async fn report_success(&self, name: &str, duration: u64, result: &str); + async fn report_error(&self, name: &str, error: &str); } #[async_trait] @@ -53,7 +52,9 @@ where R: std::fmt::Debug, for<'a> P: Deserialize<'a> + std::fmt::Debug, { - info_with_fields!("running consumer", consumer = name, queue = queue_name.to_string(), routing_key = routing_key.unwrap_or("")); + if routing_key.is_none() { + info_with_fields!("running consumer", consumer = queue_name.to_string()); + } stream_reader .read::( queue_name, diff --git a/crates/streamer/src/lib.rs b/crates/streamer/src/lib.rs index aeba786f7..22c658ede 100644 --- a/crates/streamer/src/lib.rs +++ b/crates/streamer/src/lib.rs @@ -7,6 +7,50 @@ pub mod steam_producer_queue; pub mod stream_producer; pub mod stream_reader; +use std::error::Error; +use std::future::Future; +use std::time::Duration; + +use gem_tracing::info_with_fields; + +#[derive(Clone)] +pub struct Retry { + pub delay: Duration, + pub timeout: Duration, +} + +impl Retry { + pub fn new(delay: Duration, timeout: Duration) -> Self { + Self { delay, timeout } + } +} + +pub async fn with_retry(retry: &Retry, name: &str, mut f: F) -> Result> +where + F: FnMut() -> Fut, + Fut: Future>>, +{ + let mut delay = retry.delay; + let mut attempt: u32 = 0; + loop { + attempt += 1; + match f().await { + Ok(result) => return Ok(result), + Err(err) => { + info_with_fields!( + "rabbitmq connect retry", + connection = name, + attempt = attempt, + delay_secs = delay.as_secs(), + error = err.to_string() + ); + tokio::time::sleep(delay).await; + delay = (delay * 2).min(retry.timeout); + } + } + } +} + pub use connection::StreamConnection; pub use consumer::ConsumerConfig; pub use consumer::ConsumerStatusReporter; diff --git a/crates/streamer/src/payload.rs b/crates/streamer/src/payload.rs index abde09766..5f752ebf8 100644 --- a/crates/streamer/src/payload.rs +++ b/crates/streamer/src/payload.rs @@ -1,6 +1,5 @@ use primitives::{ - AssetAddress, AssetId, Chain, ChainAddress, ChartData, FailedNotification, FiatProviderName, FiatTransaction, GorushNotification, NotificationType, PriceData, Subscription, - Transaction, + AssetAddress, AssetId, Chain, ChainAddress, ChartData, FailedNotification, FiatProviderName, FiatTransaction, GorushNotification, NotificationType, PriceData, Transaction, }; use serde::{Deserialize, Serialize}; use std::fmt; @@ -167,12 +166,6 @@ impl fmt::Display for ChainAddressPayload { } } -impl From for ChainAddressPayload { - fn from(subscription: Subscription) -> Self { - Self::new(ChainAddress::new(subscription.chain, subscription.address)) - } -} - impl From for ChainAddressPayload { fn from(chain_address: ChainAddress) -> Self { Self::new(chain_address) diff --git a/crates/streamer/src/queue.rs b/crates/streamer/src/queue.rs index 502014cf3..880046901 100644 --- a/crates/streamer/src/queue.rs +++ b/crates/streamer/src/queue.rs @@ -25,8 +25,6 @@ pub enum QueueName { FetchNFTCollection, // Fetch and store nft collection assets FetchNFTCollectionAssets, - // Store assets associations to address_assets table - StoreAssetsAssociations, // Fetch address token balances from providers and store to db FetchTokenAssociations, // Fetch address coin balances from providers and store to db @@ -79,7 +77,6 @@ impl fmt::Display for QueueName { QueueName::FetchBlocks => write!(f, "fetch_blocks"), QueueName::FetchNFTCollection => write!(f, "fetch_nft_collection"), QueueName::FetchNFTCollectionAssets => write!(f, "fetch_nft_collection_assets"), - QueueName::StoreAssetsAssociations => write!(f, "store_assets_associations"), QueueName::FetchTokenAssociations => write!(f, "fetch_token_associations"), QueueName::FetchCoinAssociations => write!(f, "fetch_coin_associations"), QueueName::FetchAddressTransactions => write!(f, "fetch_address_transactions"), diff --git a/crates/streamer/src/steam_producer_queue.rs b/crates/streamer/src/steam_producer_queue.rs index d6310f7fa..5659543f5 100644 --- a/crates/streamer/src/steam_producer_queue.rs +++ b/crates/streamer/src/steam_producer_queue.rs @@ -3,8 +3,8 @@ use std::error::Error; use primitives::{AssetId, Chain}; use crate::{ - AssetsAddressPayload, ChainAddressPayload, ChartsPayload, ExchangeName, FetchAssetsPayload, FetchBlocksPayload, FetchPricesPayload, InAppNotificationPayload, - NotificationsFailedPayload, NotificationsPayload, PricesPayload, QueueName, RewardsNotificationPayload, RewardsRedemptionPayload, StreamProducer, TransactionsPayload, + ChainAddressPayload, ChartsPayload, ExchangeName, FetchAssetsPayload, FetchBlocksPayload, FetchPricesPayload, InAppNotificationPayload, NotificationsFailedPayload, + NotificationsPayload, PricesPayload, QueueName, RewardsNotificationPayload, RewardsRedemptionPayload, StreamProducer, TransactionsPayload, }; #[async_trait::async_trait] @@ -19,7 +19,6 @@ pub trait StreamProducerQueue { async fn publish_rewards_events(&self, payload: Vec) -> Result>; async fn publish_rewards_redemption(&self, payload: RewardsRedemptionPayload) -> Result>; async fn publish_notifications_failed(&self, payload: NotificationsFailedPayload) -> Result>; - async fn publish_store_assets_addresses_associations(&self, payload: AssetsAddressPayload) -> Result>; async fn publish_prices(&self, payload: PricesPayload) -> Result>; async fn publish_charts(&self, payload: ChartsPayload) -> Result>; async fn publish_blocks(&self, chain: Chain, blocks: &[u64]) -> Result<(), Box>; @@ -99,13 +98,6 @@ impl StreamProducerQueue for StreamProducer { self.publish(QueueName::NotificationsFailed, &payload).await } - async fn publish_store_assets_addresses_associations(&self, payload: AssetsAddressPayload) -> Result> { - if payload.values.is_empty() { - return Ok(true); - } - self.publish(QueueName::StoreAssetsAssociations, &payload).await - } - async fn publish_prices(&self, payload: PricesPayload) -> Result> { if payload.prices.is_empty() { return Ok(true); diff --git a/crates/streamer/src/stream_producer.rs b/crates/streamer/src/stream_producer.rs index e784e98cd..38a43e5e0 100644 --- a/crates/streamer/src/stream_producer.rs +++ b/crates/streamer/src/stream_producer.rs @@ -1,10 +1,8 @@ use std::error::Error; -use std::time::Duration; -use gem_tracing::info_with_fields; use lapin::{BasicProperties, Channel, Connection, ConnectionProperties, ExchangeKind, options::*, publisher_confirm::Confirmation, types::FieldTable}; -use crate::{ExchangeName, QueueName, StreamConnection}; +use crate::{ExchangeName, QueueName, Retry, StreamConnection, with_retry}; const ROUTING_KEY_EXCHANGE_SUFFIX: &str = "_exchange"; const MAX_QUEUE_BYTES: i64 = 1_000_000_000; @@ -12,17 +10,12 @@ const MAX_QUEUE_BYTES: i64 = 1_000_000_000; #[derive(Clone)] pub struct StreamProducerConfig { pub url: String, - pub retry_delay: Duration, - pub retry_max_delay: Duration, + pub retry: Retry, } impl StreamProducerConfig { - pub fn new(url: String, retry_delay: Duration, retry_max_delay: Duration) -> Self { - Self { - url, - retry_delay, - retry_max_delay, - } + pub fn new(url: String, retry: Retry) -> Self { + Self { url, retry } } } @@ -39,30 +32,8 @@ pub struct StreamProducer { impl StreamProducer { pub async fn new(config: &StreamProducerConfig, connection_name: &str) -> Result> { - let options = ConnectionProperties::default().with_connection_name(connection_name.into()); - let mut delay = config.retry_delay; - let mut attempt: u32 = 0; - - loop { - attempt += 1; - match Connection::connect(&config.url, options.clone()).await { - Ok(connection) => { - let channel = connection.create_channel().await?; - return Ok(Self { channel }); - } - Err(err) => { - info_with_fields!( - "rabbitmq connect retry", - connection = connection_name, - attempt = attempt, - delay_secs = delay.as_secs(), - error = format!("{err}") - ); - tokio::time::sleep(delay).await; - delay = (delay * 2).min(config.retry_max_delay); - } - } - } + let channel = with_retry(&config.retry, connection_name, || Self::try_connect(&config.url, connection_name)).await?; + Ok(Self { channel }) } pub async fn from_connection(connection: &StreamConnection) -> Result> { @@ -70,6 +41,13 @@ impl StreamProducer { Ok(Self { channel }) } + async fn try_connect(url: &str, name: &str) -> Result> { + let options = ConnectionProperties::default().with_connection_name(name.to_string().into()); + let connection = Connection::connect(url, options).await?; + let channel = connection.create_channel().await?; + Ok(channel) + } + // Queue methods pub async fn declare_queue(&self, name: &str) -> Result<(), Box> { diff --git a/crates/streamer/src/stream_reader.rs b/crates/streamer/src/stream_reader.rs index bc123414b..86eedf28d 100644 --- a/crates/streamer/src/stream_reader.rs +++ b/crates/streamer/src/stream_reader.rs @@ -1,43 +1,56 @@ use std::error::Error; use futures::StreamExt; -use gem_tracing::error_with_fields; +use gem_tracing::{error_with_fields, info_with_fields}; use lapin::{Channel, Connection, ConnectionProperties, options::*, types::FieldTable}; use serde::de::DeserializeOwned; use tokio::sync::watch; -use crate::{QueueName, StreamConnection}; +use crate::{QueueName, Retry, StreamConnection, with_retry}; pub type ShutdownReceiver = watch::Receiver; +#[derive(Clone)] pub struct StreamReaderConfig { pub url: String, pub name: String, pub prefetch: u16, + pub retry: Retry, } impl StreamReaderConfig { - pub fn new(url: String, name: String, prefetch: u16) -> Self { - Self { url, name, prefetch } + pub fn new(url: String, name: String, prefetch: u16, retry: Retry) -> Self { + Self { url, name, prefetch, retry } } } pub struct StreamReader { + config: StreamReaderConfig, channel: Channel, } impl StreamReader { pub async fn new(config: StreamReaderConfig) -> Result> { - let connection = Connection::connect(&config.url, ConnectionProperties::default().with_connection_name(config.name.into())).await?; - let channel = connection.create_channel().await?; - channel.basic_qos(config.prefetch, BasicQosOptions { global: false }).await?; - Ok(Self { channel }) + let channel = with_retry(&config.retry, &config.name, || Self::try_connect(&config)).await?; + Ok(Self { config, channel }) + } + + pub async fn from_connection(connection: &StreamConnection, config: StreamReaderConfig) -> Result> { + let config = StreamReaderConfig { + url: connection.url().to_string(), + name: connection.name().to_string(), + ..config + }; + let channel = with_retry(&config.retry, &config.name, || Self::try_connect(&config)).await?; + Ok(Self { config, channel }) } - pub async fn from_connection(connection: &StreamConnection, prefetch: u16) -> Result> { + async fn try_connect(config: &StreamReaderConfig) -> Result> { + let options = ConnectionProperties::default().with_connection_name(config.name.clone().into()); + let connection = Connection::connect(&config.url, options).await?; let channel = connection.create_channel().await?; - channel.basic_qos(prefetch, BasicQosOptions { global: false }).await?; - Ok(Self { channel }) + channel.basic_qos(config.prefetch, BasicQosOptions { global: false }).await?; + Ok(channel) } pub async fn close(self) -> Result<(), Box> { @@ -45,7 +58,40 @@ impl StreamReader { Ok(()) } - pub async fn read(&mut self, queue: QueueName, routing_key: Option<&str>, callback: F, shutdown_rx: ShutdownReceiver) -> Result<(), Box> + async fn reconnect(&mut self, shutdown_rx: &ShutdownReceiver) -> Result> { + let mut delay = self.config.retry.delay; + let mut attempt: u32 = 0; + loop { + if *shutdown_rx.borrow() { + return Ok(false); + } + attempt += 1; + match Self::try_connect(&self.config).await { + Ok(channel) => { + self.channel = channel; + info_with_fields!("rabbitmq reconnected", connection = self.config.name.as_str(), attempt = attempt); + return Ok(true); + } + Err(err) => { + info_with_fields!( + "rabbitmq reconnect retry", + connection = self.config.name.as_str(), + attempt = attempt, + delay_secs = delay.as_secs(), + error = err.to_string() + ); + let mut rx = shutdown_rx.clone(); + tokio::select! { + _ = tokio::time::sleep(delay) => {} + _ = rx.changed() => return Ok(false), + } + delay = (delay * 2).min(self.config.retry.timeout); + } + } + } + } + + pub async fn read(&mut self, queue: QueueName, routing_key: Option<&str>, mut callback: F, shutdown_rx: ShutdownReceiver) -> Result<(), Box> where T: DeserializeOwned, F: FnMut(T) -> Result<(), Box>, @@ -54,38 +100,56 @@ impl StreamReader { Some(key) => (format!("{}.{}", queue, key), format!("consumer-{}-{}", queue, key)), None => (queue.to_string(), format!("consumer-{queue}")), }; - let mut consumer = self - .channel - .basic_consume( - queue_name.as_str(), - consumer_tag.as_str(), - BasicConsumeOptions { - no_local: false, - no_ack: false, - exclusive: false, - nowait: false, - }, - FieldTable::default(), - ) - .await?; - - self.consume(&mut consumer, callback, shutdown_rx).await + + loop { + if *shutdown_rx.borrow() { + break; + } + + let consumer_result = self + .channel + .basic_consume(queue_name.as_str(), consumer_tag.as_str(), BasicConsumeOptions::default(), FieldTable::default()) + .await; + + let mut consumer = match consumer_result { + Ok(c) => c, + Err(e) => { + info_with_fields!("consumer setup failed", connection = self.config.name.as_str(), error = format!("{e}")); + if !self.reconnect(&shutdown_rx).await? { + break; + } + continue; + } + }; + + let result = self.consume::(&mut consumer, &mut callback, shutdown_rx.clone()).await; + if let Ok(true) = result { + break; + } + let error = result.err().map(|e| e.to_string()); + info_with_fields!( + "consumer reconnecting", + connection = self.config.name.as_str(), + error = error.as_deref().unwrap_or("stream ended") + ); + if !self.reconnect(&shutdown_rx).await? { + break; + } + } + + Ok(()) } - async fn consume(&mut self, consumer: &mut lapin::Consumer, mut callback: F, shutdown_rx: ShutdownReceiver) -> Result<(), Box> + async fn consume(&mut self, consumer: &mut lapin::Consumer, callback: &mut F, shutdown_rx: ShutdownReceiver) -> Result> where T: DeserializeOwned, F: FnMut(T) -> Result<(), Box>, { loop { - if *shutdown_rx.borrow() { - break; - } - let mut rx = shutdown_rx.clone(); let delivery = tokio::select! { d = consumer.next() => d, - _ = rx.changed() => break, + _ = rx.changed() => return Ok(true), }; match delivery { @@ -104,11 +168,9 @@ impl StreamReader { } } Some(Err(e)) => return Err(Box::new(e)), - None => break, + None => return Ok(false), } } - - Ok(()) } async fn ack(&self, delivery_tag: u64) -> Result<(), Box> { diff --git a/crates/support/src/client.rs b/crates/support/src/client.rs index 24d388406..1d178820d 100644 --- a/crates/support/src/client.rs +++ b/crates/support/src/client.rs @@ -2,7 +2,7 @@ use crate::ChatwootWebhookPayload; use localizer::LanguageLocalizer; use primitives::{Device, GorushNotification, PushNotification, PushNotificationTypes, push_notification::PushNotificationSupport}; use std::error::Error; -use storage::database::support::SupportStore; +use storage::database::devices::DevicesStore; use storage::{Database, OptionalExtension}; use streamer::{NotificationsPayload, StreamProducer, StreamProducerQueue}; @@ -16,11 +16,11 @@ impl SupportClient { Self { database, stream_producer } } - pub fn get_device(&self, support_device_id: &str) -> Result, Box> { - Ok(self.database.support()?.get_support_device(support_device_id).optional()?.map(|d| d.as_primitive())) + pub fn get_device(&self, device_id: &str) -> Result, Box> { + Ok(DevicesStore::get_device(&mut self.database.client()?, device_id).optional()?.map(|d| d.as_primitive())) } - pub async fn handle_message_created(&self, device: &Device, support_device_id: &str, payload: &ChatwootWebhookPayload) -> Result> { + pub async fn handle_message_created(&self, device: &Device, payload: &ChatwootWebhookPayload) -> Result> { let notifications_count = if let Some(notification) = Self::build_notification(device, payload) { self.stream_producer.publish_notifications_support(NotificationsPayload::new(vec![notification])).await?; 1 @@ -28,19 +28,10 @@ impl SupportClient { 0 }; - self.update_unread(support_device_id, payload)?; Ok(notifications_count) } - pub fn handle_conversation_updated(&self, support_device_id: &str, payload: &ChatwootWebhookPayload) -> Result<(), Box> { - self.update_unread(support_device_id, payload)?; - Ok(()) - } - - fn update_unread(&self, support_device_id: &str, payload: &ChatwootWebhookPayload) -> Result<(), Box> { - if let Some(unread) = payload.get_unread() { - SupportStore::support_update_unread(&mut self.database.client()?, support_device_id, unread)?; - } + pub fn handle_conversation_updated(&self, _payload: &ChatwootWebhookPayload) -> Result<(), Box> { Ok(()) } diff --git a/crates/support/src/model.rs b/crates/support/src/model.rs index f58151158..3e24f6e2f 100644 --- a/crates/support/src/model.rs +++ b/crates/support/src/model.rs @@ -51,8 +51,7 @@ pub struct Meta { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomAttributes { - #[serde(rename = "supportdeviceid", alias = "supportDeviceId", alias = "support_device_id")] - pub support_device_id: Option, + pub device_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -61,16 +60,9 @@ pub struct Sender { } impl ChatwootWebhookPayload { - pub fn get_support_device_id(&self) -> Option { - self.conversation - .as_ref() - .map(|c| &c.meta) - .or(self.meta.as_ref())? - .sender - .custom_attributes - .as_ref()? - .support_device_id - .clone() + pub fn get_device_id(&self) -> Option { + let attrs = self.conversation.as_ref().map(|c| &c.meta).or(self.meta.as_ref())?.sender.custom_attributes.as_ref()?; + attrs.device_id.clone() } pub fn get_unread(&self) -> Option { diff --git a/crates/support/tests/model_tests.rs b/crates/support/tests/model_tests.rs index b4ff55ec8..770e518e0 100644 --- a/crates/support/tests/model_tests.rs +++ b/crates/support/tests/model_tests.rs @@ -4,7 +4,7 @@ use support::ChatwootWebhookPayload; fn test_parse_conversation_updated_payload() { let payload: ChatwootWebhookPayload = serde_json::from_str(include_str!("testdata/chatwoot_conversation_updated.json")).unwrap(); assert_eq!(payload.event, "conversation_updated"); - assert_eq!(payload.get_support_device_id(), Some("test-device-id".to_string())); + assert_eq!(payload.get_device_id(), Some("test-device-id".to_string())); assert_eq!(payload.get_unread(), Some(1)); let messages = payload.get_messages(); @@ -19,18 +19,10 @@ fn test_parse_conversation_updated_payload() { } #[test] -fn test_parse_support_device_id_aliases() { +fn test_parse_device_id() { let payload: ChatwootWebhookPayload = - serde_json::from_str(r#"{"event": "conversation_updated", "meta": {"sender": {"custom_attributes": {"supportDeviceId": "test-camel"}}}}"#).unwrap(); - assert_eq!(payload.get_support_device_id(), Some("test-camel".to_string())); - - let payload: ChatwootWebhookPayload = - serde_json::from_str(r#"{"event": "conversation_updated", "meta": {"sender": {"custom_attributes": {"support_device_id": "test-snake"}}}}"#).unwrap(); - assert_eq!(payload.get_support_device_id(), Some("test-snake".to_string())); - - let payload: ChatwootWebhookPayload = - serde_json::from_str(r#"{"event": "conversation_updated", "meta": {"sender": {"custom_attributes": {"supportdeviceid": "test-lower"}}}}"#).unwrap(); - assert_eq!(payload.get_support_device_id(), Some("test-lower".to_string())); + serde_json::from_str(r#"{"event": "conversation_updated", "meta": {"sender": {"custom_attributes": {"device_id": "test-device"}}}}"#).unwrap(); + assert_eq!(payload.get_device_id(), Some("test-device".to_string())); } #[test] @@ -38,7 +30,7 @@ fn test_parse_message_created_payload() { let payload: ChatwootWebhookPayload = serde_json::from_str(include_str!("testdata/chatwoot_message_created.json")).unwrap(); assert_eq!(payload.event, "message_created"); assert_eq!(payload.content, Some("from agent".to_string())); - assert_eq!(payload.get_support_device_id(), Some("test-device-id".to_string())); + assert_eq!(payload.get_device_id(), Some("test-device-id".to_string())); assert_eq!(payload.get_unread(), Some(1)); assert!(payload.is_outgoing_message()); diff --git a/crates/support/tests/testdata/chatwoot_conversation_updated.json b/crates/support/tests/testdata/chatwoot_conversation_updated.json index 07fb1a247..3e68de970 100644 --- a/crates/support/tests/testdata/chatwoot_conversation_updated.json +++ b/crates/support/tests/testdata/chatwoot_conversation_updated.json @@ -77,7 +77,7 @@ "currency": "USD", "platform": "android", "app_version": "1.0.0", - "support_device_id": "test-device-id" + "device_id": "test-device-id" }, "email": null, "id": 1, diff --git a/crates/support/tests/testdata/chatwoot_message_created.json b/crates/support/tests/testdata/chatwoot_message_created.json index 7d20eb9fc..f03cca166 100644 --- a/crates/support/tests/testdata/chatwoot_message_created.json +++ b/crates/support/tests/testdata/chatwoot_message_created.json @@ -56,7 +56,7 @@ "currency": "USD", "platform": "android", "app_version": "1.0.0", - "supportDeviceId": "test-device-id" + "device_id": "test-device-id" }, "email": null, "id": 1, diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 8dde48d5e..b843ea81f 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -15,7 +15,12 @@ pub struct DurationMs(pub Duration); impl std::fmt::Display for DurationMs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}ms", self.0.as_millis()) + let ms = self.0.as_millis(); + if ms >= 5000 && ms.is_multiple_of(1000) { + write!(f, "{}s", ms / 1000) + } else { + write!(f, "{}ms", ms) + } } } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 9ffa6f001..2d973648f 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -2,7 +2,8 @@ mod models; mod provider; pub mod yo; -pub use models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +pub use models::{Yield, YieldDetailsRequest, EarnTransaction}; +pub use primitives::YieldProvider; pub use provider::{YieldProviderClient, Yielder}; pub use yo::{ 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/models.rs b/crates/yielder/src/models.rs index 1381ee783..2149b77c7 100644 --- a/crates/yielder/src/models.rs +++ b/crates/yielder/src/models.rs @@ -1,7 +1,6 @@ -use primitives::{AssetId, Chain, EarnPosition, EarnProvider, swap::ApprovalData}; +use primitives::{AssetId, YieldProvider}; -pub type YieldProvider = EarnProvider; -pub type YieldPosition = EarnPosition; +pub use primitives::EarnTransaction; #[derive(Debug, Clone)] pub struct Yield { @@ -22,16 +21,6 @@ impl Yield { } } -#[derive(Debug, Clone)] -pub struct YieldTransaction { - pub chain: Chain, - pub from: String, - pub to: String, - pub data: String, - pub value: Option, - pub approval: Option, -} - #[derive(Debug, Clone)] pub struct YieldDetailsRequest { pub asset_id: AssetId, diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index b051b0fdf..8012f5cea 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -1,18 +1,18 @@ use std::sync::Arc; use async_trait::async_trait; -use primitives::AssetId; +use primitives::{AssetId, DelegationBase, YieldProvider}; -use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +use crate::models::{Yield, YieldDetailsRequest, EarnTransaction}; use crate::yo::YieldError; #[async_trait] pub trait YieldProviderClient: Send + Sync { fn provider(&self) -> YieldProvider; fn yields(&self, asset_id: &AssetId) -> Vec; - 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 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)) } @@ -43,17 +43,17 @@ impl Yielder { Ok(yields) } - pub async fn deposit(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + pub async fn deposit(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { 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 { + pub async fn withdraw(&self, provider: YieldProvider, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { let provider = self.get_provider(provider)?; provider.withdraw(asset_id, wallet_address, value).await } - pub async fn positions(&self, provider: YieldProvider, request: &YieldDetailsRequest) -> Result { + pub async fn positions(&self, provider: YieldProvider, request: &YieldDetailsRequest) -> Result { let provider = self.get_provider(provider)?; provider.positions(request).await } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index b7af06f55..d0103ad09 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,10 +3,10 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy_primitives::{Address, U256}; use async_trait::async_trait; use gem_evm::jsonrpc::TransactionObject; -use num_bigint::BigInt; -use primitives::{AssetId, Chain, swap::ApprovalData}; +use num_bigint::BigUint; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, YieldProvider, swap::ApprovalData}; -use crate::models::{Yield, YieldDetailsRequest, YieldPosition, YieldProvider, YieldTransaction}; +use crate::models::{Yield, YieldDetailsRequest, EarnTransaction}; use crate::provider::YieldProviderClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; @@ -75,7 +75,7 @@ impl YieldProviderClient for YoYieldProvider { Ok(results) } - async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + async fn deposit(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { 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}"))?; @@ -86,7 +86,7 @@ impl YieldProviderClient for YoYieldProvider { Ok(convert_transaction(vault, tx, approval)) } - async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { + async fn withdraw(&self, asset_id: &AssetId, wallet_address: &str, value: &str) -> Result { 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}"))?; @@ -98,7 +98,7 @@ impl YieldProviderClient for YoYieldProvider { Ok(convert_transaction(vault, tx, approval)) } - async fn positions(&self, request: &YieldDetailsRequest) -> Result { + async fn positions(&self, request: &YieldDetailsRequest) -> Result { 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))?; @@ -107,25 +107,25 @@ impl YieldProviderClient for YoYieldProvider { 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 asset_value_string = asset_value.to_string(); - let asset_value_value = BigInt::from_str(&asset_value_string).map_err(|e| format!("invalid asset value {asset_value_string}: {e}"))?; - let share_balance_value = BigInt::from_str(&data.share_balance.to_string()).map_err(|e| format!("invalid share balance {}: {e}", data.share_balance))?; - Ok(YieldPosition { + let balance = BigUint::from_str(&asset_value.to_string()).unwrap_or_default(); + let shares = BigUint::from_str(&data.share_balance.to_string()).unwrap_or_default(); + let provider_id = self.provider().to_string(); + + Ok(DelegationBase { 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: share_balance_value, - asset_balance_value: asset_value_value, - balance: asset_value_string, - apy: None, - rewards: None, + state: DelegationState::Active, + balance, + shares, + rewards: BigUint::ZERO, + completion_date: None, + delegation_id: format!("{}-{}", provider_id, request.asset_id), + validator_id: provider_id, }) } } -fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option) -> YieldTransaction { - YieldTransaction { +fn convert_transaction(vault: YoVault, tx: TransactionObject, approval: Option) -> EarnTransaction { + EarnTransaction { chain: vault.chain, from: tx.from.unwrap_or_default(), to: tx.to, diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 8ac186080..8d35d0cee 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -6,13 +6,13 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ GemstoneError, alien::{AlienProvider, AlienProviderWrapper}, - models::{GemEarnData, GemTransactionInputType, GemTransactionLoadInput}, + models::{GemDelegationBase, GemEarnData, GemEarnType, GemTransactionInputType, GemTransactionLoadInput, GemTransactionLoadMetadata}, }; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; use primitives::{AssetId, Chain, EVMChain}; -use yielder::{GAS_LIMIT, YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, YieldTransaction, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; +use yielder::{GAS_LIMIT, YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; #[derive(uniffi::Object)] pub struct GemYielder { @@ -31,32 +31,35 @@ impl GemYielder { self.yielder.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) } - pub async fn deposit(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String, value: String) -> Result { + pub async fn deposit(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String, value: String) -> Result { self.yielder.deposit(provider, &asset, &wallet_address, &value).await.map_err(Into::into) } - pub async fn withdraw(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String, value: String) -> Result { + pub async fn withdraw(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String, value: String) -> Result { self.yielder.withdraw(provider, &asset, &wallet_address, &value).await.map_err(Into::into) } - pub async fn positions(&self, provider: GemEarnProvider, asset: AssetId, wallet_address: String) -> Result { + pub async fn positions(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String) -> Result { let request = YieldDetailsRequest { asset_id: asset, wallet_address }; self.yielder.positions(provider, &request).await.map_err(Into::into) } pub async fn build_transaction( &self, - action: GemEarnAction, - provider: GemEarnProvider, + action: GemEarnType, + provider: GemYieldProvider, asset: AssetId, wallet_address: String, value: String, nonce: u64, chain_id: u64, - ) -> Result { - let transaction = build_yield_transaction(&self.yielder, &action, provider, &asset, &wallet_address, &value).await?; + ) -> Result { + let transaction = match action { + GemEarnType::Deposit(_) => self.yielder.deposit(provider, &asset, &wallet_address, &value).await?, + GemEarnType::Withdraw(_) => self.yielder.withdraw(provider, &asset, &wallet_address, &value).await?, + }; - Ok(GemYieldTransactionData { + Ok(GemEarnTransactionData { transaction, nonce, chain_id, @@ -85,49 +88,37 @@ pub(crate) fn build_yielder(rpc_provider: Arc) -> Result Result { - match &input.input_type { - GemTransactionInputType::Earn { asset, action, data } => { - 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::Earn { - asset: asset.clone(), - action: action.clone(), - 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()), - }, - }, - 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) - } + match (&input.input_type, &input.metadata) { + ( + GemTransactionInputType::Earn { asset, earn_type }, + GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data: None }, + ) => { + let transaction = match earn_type { + GemEarnType::Deposit(_) => yielder.deposit(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?, + GemEarnType::Withdraw(_) => yielder.withdraw(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?, + }; + + Ok(GemTransactionLoadInput { + input_type: input.input_type.clone(), + 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: GemTransactionLoadMetadata::Evm { + nonce: *nonce, + chain_id: *chain_id, + earn_data: Some(GemEarnData { + provider: Some(earn_type.provider_id().to_string()), + contract_address: Some(transaction.to), + call_data: Some(transaction.data), + approval: transaction.approval, + gas_limit: Some(GAS_LIMIT.to_string()), + }), + }, + }) } _ => Ok(input), } } - -async fn build_yield_transaction( - yielder: &Yielder, - action: &GemEarnAction, - provider: YieldProvider, - asset: &AssetId, - wallet_address: &str, - value: &str, -) -> Result { - match action { - GemEarnAction::Deposit => Ok(yielder.deposit(provider, asset, wallet_address, value).await?), - GemEarnAction::Withdraw => Ok(yielder.withdraw(provider, asset, wallet_address, value).await?), - } -} diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index 0c33e20a3..b0142cd1f 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,20 +1,18 @@ -use primitives::{AssetId, EarnPosition, EarnProvider}; -use yielder::{Yield, YieldTransaction}; +use primitives::{AssetId, YieldProvider}; +use yielder::{Yield, EarnTransaction}; -use crate::models::GemBigInt; use crate::models::swap::GemApprovalData; -pub use crate::models::transaction::GemEarnAction; -pub type GemEarnProvider = EarnProvider; +pub type GemYieldProvider = YieldProvider; #[uniffi::remote(Enum)] -pub enum GemEarnProvider { +pub enum GemYieldProvider { Yo, } #[derive(Debug, Clone, uniffi::Record)] -pub struct GemYieldTransactionData { - pub transaction: GemYieldTransaction, +pub struct GemEarnTransactionData { + pub transaction: GemEarnTransaction, pub nonce: u64, pub chain_id: u64, pub gas_limit: String, @@ -26,14 +24,14 @@ pub type GemYield = Yield; pub struct GemYield { pub name: String, pub asset_id: AssetId, - pub provider: GemEarnProvider, + pub provider: GemYieldProvider, pub apy: Option, } -pub type GemYieldTransaction = YieldTransaction; +pub type GemEarnTransaction = EarnTransaction; #[uniffi::remote(Record)] -pub struct GemYieldTransaction { +pub struct GemEarnTransaction { pub chain: primitives::Chain, pub from: String, pub to: String, @@ -41,18 +39,3 @@ pub struct GemYieldTransaction { pub value: Option, pub approval: Option, } - -pub type GemEarnPosition = EarnPosition; - -#[uniffi::remote(Record)] -pub struct GemEarnPosition { - pub asset_id: AssetId, - pub provider: GemEarnProvider, - pub vault_token_address: String, - pub asset_token_address: String, - pub vault_balance_value: GemBigInt, - pub asset_balance_value: GemBigInt, - pub balance: String, - pub apy: Option, - pub rewards: Option, -} diff --git a/gemstone/src/models/perpetual.rs b/gemstone/src/models/perpetual.rs index 5da597695..ee8b24037 100644 --- a/gemstone/src/models/perpetual.rs +++ b/gemstone/src/models/perpetual.rs @@ -5,7 +5,7 @@ use gem_hypercore::models::order::OpenOrder; use gem_hypercore::models::websocket::{HyperliquidSocketMessage, PositionsDiff}; use primitives::{ Asset, AssetId, PerpetualDirection, PerpetualMarginType, PerpetualOrderType, PerpetualPosition, PerpetualProvider, PerpetualTriggerOrder, - chart::{ChartCandleStick, ChartDateValue}, + chart::{ChartCandleStick, ChartCandleUpdate, ChartDateValue}, perpetual::{Perpetual, PerpetualBalance, PerpetualData, PerpetualMetadata, PerpetualPositionsSummary}, }; @@ -19,6 +19,7 @@ pub type GemPerpetualPosition = PerpetualPosition; pub type GemPerpetual = Perpetual; pub type GemPerpetualMetadata = PerpetualMetadata; pub type GemChartCandleStick = ChartCandleStick; +pub type GemChartCandleUpdate = ChartCandleUpdate; pub type GemChartDateValue = ChartDateValue; pub type GemPerpetualData = PerpetualData; @@ -105,7 +106,6 @@ pub struct GemPerpetualMetadata { #[uniffi::remote(Record)] pub struct GemChartCandleStick { pub date: DateTime, - pub interval: String, pub open: f64, pub high: f64, pub low: f64, @@ -113,6 +113,13 @@ pub struct GemChartCandleStick { pub volume: f64, } +#[uniffi::remote(Record)] +pub struct GemChartCandleUpdate { + pub coin: String, + pub interval: String, + pub candle: ChartCandleStick, +} + #[uniffi::remote(Record)] pub struct GemChartDateValue { pub date: DateTime, @@ -141,7 +148,7 @@ pub type GemHyperliquidSocketMessage = HyperliquidSocketMessage; pub enum GemHyperliquidSocketMessage { ClearinghouseState { balance: PerpetualBalance, positions: Vec }, OpenOrders { orders: Vec }, - Candle { candle: ChartCandleStick }, + Candle { candle: ChartCandleUpdate }, AllMids { prices: HashMap }, SubscriptionResponse { subscription_type: String }, Unknown, diff --git a/gemstone/src/models/stake.rs b/gemstone/src/models/stake.rs index 641b98bd9..29fd17a53 100644 --- a/gemstone/src/models/stake.rs +++ b/gemstone/src/models/stake.rs @@ -1,6 +1,6 @@ use crate::models::custom_types::{DateTimeUtc, GemBigUint}; use primitives::stake_type::{FreezeType, Resource}; -use primitives::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, Price, StakeChain}; +use primitives::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, Price, StakeChain}; pub type GemFreezeType = FreezeType; pub type GemResource = Resource; @@ -8,6 +8,7 @@ pub type GemDelegation = Delegation; pub type GemDelegationBase = DelegationBase; pub type GemDelegationValidator = DelegationValidator; pub type GemDelegationState = DelegationState; +pub type GemGrowthProviderType = GrowthProviderType; pub type GemPrice = Price; pub type GemStakeChain = StakeChain; @@ -50,6 +51,12 @@ pub enum GemDelegationState { AwaitingWithdrawal, } +#[uniffi::remote(Enum)] +pub enum GemGrowthProviderType { + Stake, + Earn, +} + #[uniffi::remote(Record)] pub struct GemDelegationValidator { pub chain: Chain, @@ -58,6 +65,7 @@ pub struct GemDelegationValidator { pub is_active: bool, pub commission: f64, pub apr: f64, + pub provider_type: GemGrowthProviderType, } #[uniffi::remote(Record)] diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 9807bc887..2a5ed268c 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -2,9 +2,10 @@ use crate::models::*; use num_bigint::BigInt; use primitives::stake_type::FreezeData; use primitives::{ - AccountDataType, Asset, EarnAction, EarnData, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, StakeType, + AccountDataType, Asset, EarnData, EarnType, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, Resource, StakeType, TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, - TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, UInt64, WalletConnectionSessionAppMetadata, + TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, + UInt64, WalletConnectionSessionAppMetadata, perpetual::{CancelOrderData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, TPSLOrderData}, }; use std::collections::HashMap; @@ -23,6 +24,27 @@ pub type GemTransactionState = TransactionState; pub type GemTransactionChange = TransactionChange; pub type GemTransactionUpdate = TransactionUpdate; pub type GemTransactionType = TransactionType; +pub type GemTronVote = TronVote; +pub type GemTronUnfreeze = TronUnfreeze; +pub type GemTronStakeData = TronStakeData; + +#[uniffi::remote(Record)] +pub struct TronVote { + pub validator: String, + pub count: u64, +} + +#[uniffi::remote(Record)] +pub struct TronUnfreeze { + pub resource: Resource, + pub amount: u64, +} + +#[uniffi::remote(Enum)] +pub enum TronStakeData { + Votes(Vec), + Unfreeze(Vec), +} #[uniffi::remote(Enum)] pub enum PerpetualDirection { @@ -235,14 +257,6 @@ pub enum PerpetualType { Reduce(PerpetualReduceData), } -pub type GemEarnAction = EarnAction; - -#[uniffi::remote(Enum)] -pub enum EarnAction { - Deposit, - Withdraw, -} - pub type GemEarnData = EarnData; #[uniffi::remote(Record)] @@ -254,6 +268,14 @@ pub struct EarnData { pub gas_limit: Option, } +pub type GemEarnType = EarnType; + +#[uniffi::remote(Enum)] +pub enum EarnType { + Deposit(GemDelegationValidator), + Withdraw(GemDelegation), +} + #[derive(Debug, Clone, uniffi::Enum)] #[allow(clippy::large_enum_variant)] pub enum GemTransactionInputType { @@ -295,8 +317,7 @@ pub enum GemTransactionInputType { }, Earn { asset: GemAsset, - action: GemEarnAction, - data: GemEarnData, + earn_type: GemEarnType, }, } @@ -440,7 +461,7 @@ pub enum GemTransactionLoadMetadata { transaction_tree_root: String, parent_hash: String, witness_address: String, - votes: HashMap, + stake_data: GemTronStakeData, }, Sui { message_bytes: String, @@ -522,7 +543,7 @@ impl From for GemTransactionLoadMetadata { transaction_tree_root, parent_hash, witness_address, - votes, + stake_data, } => GemTransactionLoadMetadata::Tron { block_number, block_version, @@ -530,7 +551,7 @@ impl From for GemTransactionLoadMetadata { transaction_tree_root, parent_hash, witness_address, - votes, + stake_data, }, TransactionLoadMetadata::Sui { message_bytes } => GemTransactionLoadMetadata::Sui { message_bytes }, TransactionLoadMetadata::Hyperliquid { order } => GemTransactionLoadMetadata::Hyperliquid { order }, @@ -610,7 +631,7 @@ impl From for TransactionLoadMetadata { transaction_tree_root, parent_hash, witness_address, - votes, + stake_data, } => TransactionLoadMetadata::Tron { block_number, block_version, @@ -618,7 +639,7 @@ impl From for TransactionLoadMetadata { transaction_tree_root, parent_hash, witness_address, - votes, + stake_data, }, GemTransactionLoadMetadata::Sui { message_bytes } => TransactionLoadMetadata::Sui { message_bytes }, GemTransactionLoadMetadata::Hyperliquid { order } => TransactionLoadMetadata::Hyperliquid { order }, @@ -677,7 +698,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::Earn(asset, action, data) => GemTransactionInputType::Earn { asset, action, data }, + TransactionInputType::Earn(asset, earn_type) => GemTransactionInputType::Earn { asset, earn_type }, } } } @@ -831,7 +852,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::Earn { asset, action, data } => TransactionInputType::Earn(asset, action, data), + GemTransactionInputType::Earn { asset, earn_type } => TransactionInputType::Earn(asset, earn_type), } } } From 35d3a91ef4308cc9307d27199ae0648c9c203e9a Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:35:43 +0200 Subject: [PATCH 39/43] Update transaction_load_metadata.rs --- crates/primitives/src/transaction_load_metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index e5d09eb6e..33f77acab 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{UTXO, earn_data::EarnData, solana_token_program::SolanaTokenProgramId, stake_type::{StakeData, TronStakeData}}; +use crate::{UTXO, earn_data::EarnData, solana_token_program::SolanaTokenProgramId, stake_type::TronStakeData}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HyperliquidOrder { From 74afe5db17cb2b7220868f86366e9ae443b51c73 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:54:45 +0200 Subject: [PATCH 40/43] Add earn balance, providers, and positions to GemGateway - Add `earn` field to Balance/GemBalance structs - Add `get_balance_earn`, `get_earn_providers`, `get_earn_positions` to GemGateway - Move concurrent position fetching into Yielder.positions_for_chain() - Add yields_for_chain() to YieldProviderClient trait and YoYieldProvider - Make build_yielder infallible (gracefully skip unavailable chains) - Remove GemYielder FFI wrapper (all earn operations now go through GemGateway) - Remove GemEarnTransaction, GemEarnTransactionData remote types (unused) - Remove dead code: Yielder.yields_for_asset() --- Cargo.lock | 1 + .../gem_tron/src/provider/balances_mapper.rs | 1 + crates/primitives/src/asset_balance.rs | 20 +++++ crates/yielder/Cargo.toml | 1 + crates/yielder/src/provider.rs | 34 +++++-- crates/yielder/src/yo/provider.rs | 10 ++- gemstone/src/gateway/mod.rs | 43 +++++++-- gemstone/src/gem_yielder/mod.rs | 88 ++++--------------- gemstone/src/gem_yielder/remote_types.rs | 24 +---- gemstone/src/models/balance.rs | 1 + 10 files changed, 112 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a990c8a47..0c9c13c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9519,6 +9519,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "async-trait", + "futures", "gem_client", "gem_evm", "gem_jsonrpc", diff --git a/crates/gem_tron/src/provider/balances_mapper.rs b/crates/gem_tron/src/provider/balances_mapper.rs index fe47fba40..a3c1e9e19 100644 --- a/crates/gem_tron/src/provider/balances_mapper.rs +++ b/crates/gem_tron/src/provider/balances_mapper.rs @@ -94,6 +94,7 @@ fn new_stake_balance( pending_unconfirmed: BigUint::from(0u32), rewards, reserved: BigUint::from(0u32), + earn: BigUint::from(0u32), withdrawable: BigUint::from(0u32), metadata: Some(metadata), } diff --git a/crates/primitives/src/asset_balance.rs b/crates/primitives/src/asset_balance.rs index 0fc871d62..05ae42117 100644 --- a/crates/primitives/src/asset_balance.rs +++ b/crates/primitives/src/asset_balance.rs @@ -58,6 +58,14 @@ impl AssetBalance { is_active: true, } } + + pub fn new_earn(asset_id: AssetId, earn: BigUint) -> Self { + Self { + asset_id, + balance: Balance::earn_balance(earn), + is_active: true, + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -71,6 +79,7 @@ pub struct Balance { pub pending_unconfirmed: BigUint, pub rewards: BigUint, pub reserved: BigUint, + pub earn: BigUint, pub withdrawable: BigUint, pub metadata: Option, } @@ -97,6 +106,7 @@ impl Balance { pending_unconfirmed: BigUint::from(0u32), rewards: BigUint::from(0u32), reserved: BigUint::from(0u32), + earn: BigUint::from(0u32), withdrawable: BigUint::from(0u32), metadata: None, } @@ -112,6 +122,7 @@ impl Balance { pending: BigUint::from(0u32), rewards: BigUint::from(0u32), reserved: BigUint::from(0u32), + earn: BigUint::from(0u32), withdrawable: BigUint::from(0u32), metadata: None, } @@ -127,6 +138,7 @@ impl Balance { pending: BigUint::from(0u32), pending_unconfirmed: BigUint::from(0u32), rewards: BigUint::from(0u32), + earn: BigUint::from(0u32), withdrawable: BigUint::from(0u32), metadata: None, } @@ -146,8 +158,16 @@ impl Balance { pending_unconfirmed: BigUint::from(0u32), rewards: rewards.unwrap_or(BigUint::from(0u32)), reserved: BigUint::from(0u32), + earn: BigUint::from(0u32), withdrawable: BigUint::from(0u32), metadata, } } + + pub fn earn_balance(earn: BigUint) -> Self { + Self { + earn, + ..Self::coin_balance(BigUint::from(0u32)) + } + } } diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index 166b927ee..a044e9b78 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -22,6 +22,7 @@ gem_jsonrpc = { path = "../gem_jsonrpc" } primitives = { path = "../primitives" } serde_serializers = { path = "../serde_serializers" } async-trait = { workspace = true } +futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 8012f5cea..7118fff54 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -1,15 +1,19 @@ use std::sync::Arc; use async_trait::async_trait; -use primitives::{AssetId, DelegationBase, YieldProvider}; +use primitives::{AssetId, Chain, DelegationBase, YieldProvider}; -use crate::models::{Yield, YieldDetailsRequest, EarnTransaction}; +use crate::models::{EarnTransaction, Yield, YieldDetailsRequest}; use crate::yo::YieldError; #[async_trait] pub trait YieldProviderClient: Send + Sync { fn provider(&self) -> YieldProvider; fn yields(&self, asset_id: &AssetId) -> Vec; + fn yields_for_chain(&self, _chain: Chain) -> Vec { + vec![] + } + 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; @@ -27,10 +31,6 @@ impl Yielder { Self { providers } } - pub fn yields_for_asset(&self, asset_id: &AssetId) -> Vec { - self.providers.iter().flat_map(|provider| provider.yields(asset_id)).collect() - } - pub async fn yields_for_asset_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { let mut yields = Vec::new(); for provider in &self.providers { @@ -58,6 +58,28 @@ impl Yielder { provider.positions(request).await } + pub fn yields_for_chain(&self, chain: Chain) -> Vec { + self.providers.iter().flat_map(|p| p.yields_for_chain(chain)).collect() + } + + pub async fn positions_for_chain(&self, chain: Chain, address: &str) -> Vec<(Yield, DelegationBase)> { + let futures: Vec<_> = self + .yields_for_chain(chain) + .into_iter() + .map(|y| { + let address = address.to_string(); + async move { + let request = YieldDetailsRequest { + asset_id: y.asset_id.clone(), + wallet_address: address, + }; + self.positions(y.provider, &request).await.ok().map(|d| (y, d)) + } + }) + .collect(); + futures::future::join_all(futures).await.into_iter().flatten().collect() + } + fn get_provider(&self, provider: YieldProvider) -> Result, YieldError> { self.providers .iter() diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index d0103ad09..567cc48d5 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -6,7 +6,7 @@ use gem_evm::jsonrpc::TransactionObject; use num_bigint::BigUint; use primitives::{AssetId, Chain, DelegationBase, DelegationState, YieldProvider, swap::ApprovalData}; -use crate::models::{Yield, YieldDetailsRequest, EarnTransaction}; +use crate::models::{EarnTransaction, Yield, YieldDetailsRequest}; use crate::provider::YieldProviderClient; use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; @@ -66,6 +66,14 @@ impl YieldProviderClient for YoYieldProvider { .collect() } + fn yields_for_chain(&self, chain: Chain) -> Vec { + self.vaults + .iter() + .filter(|vault| vault.chain == chain) + .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_for_asset(asset_id) { diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 217496f96..4ef0a2152 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -10,7 +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::gem_yielder::{GemYield, build_yielder, prepare_yield_input}; use crate::models::*; use crate::network::JsonRpcClient; use chain_traits::ChainTraits; @@ -32,7 +32,10 @@ use gem_tron::rpc::{client::TronClient, trongrid::client::TronGridClient}; use gem_xrp::rpc::client::XRPClient; use std::sync::Arc; -use primitives::{BitcoinChain, Chain, ChartPeriod, EVMChain, ScanAddressTarget, ScanTransactionPayload, TransactionPreloadInput, chain_cosmos::CosmosChain}; +use num_bigint::BigUint; +use primitives::{ + AssetBalance, AssetId, BitcoinChain, Chain, ChartPeriod, EVMChain, ScanAddressTarget, ScanTransactionPayload, TransactionPreloadInput, chain_cosmos::CosmosChain, +}; use yielder::Yielder; #[uniffi::export(with_foreign)] @@ -48,7 +51,7 @@ pub struct GemGateway { pub preferences: Arc, pub secure_preferences: Arc, pub api_client: GemApiClient, - yielder: Option, + yielder: Yielder, } impl std::fmt::Debug for GemGateway { @@ -144,7 +147,7 @@ impl GemGateway { #[uniffi::constructor] pub fn new(provider: Arc, preferences: Arc, secure_preferences: Arc, api_url: String) -> Self { let api_client = GemApiClient::new(api_url, provider.clone()); - let yielder = build_yielder(provider.clone()).ok(); + let yielder = build_yielder(provider.clone()); Self { provider, preferences, @@ -184,6 +187,30 @@ impl GemGateway { Ok(balance) } + pub async fn get_balance_earn(&self, chain: Chain, address: String) -> Result, GatewayError> { + let positions = self.yielder.positions_for_chain(chain, &address).await; + Ok(positions + .into_iter() + .filter(|(_, d)| d.balance > BigUint::ZERO) + .map(|(y, d)| AssetBalance::new_earn(y.asset_id, d.balance)) + .collect()) + } + + pub async fn get_earn_providers(&self, asset_id: String) -> Result, GatewayError> { + let asset_id = AssetId::new(&asset_id).ok_or_else(|| GatewayError::NetworkError { + msg: format!("invalid asset_id: {asset_id}"), + })?; + self.yielder + .yields_for_asset_with_apy(&asset_id) + .await + .map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) + } + + pub async fn get_earn_positions(&self, chain: Chain, address: String) -> Result, GatewayError> { + let positions = self.yielder.positions_for_chain(chain, &address).await; + Ok(positions.into_iter().map(|(_, d)| d).collect()) + } + pub async fn get_staking_validators(&self, chain: Chain, apy: Option) -> Result, GatewayError> { let provider = self.provider(chain).await?; @@ -320,11 +347,9 @@ impl GemGateway { } pub async fn get_transaction_load(&self, chain: Chain, input: GemTransactionLoadInput, provider: Arc) -> Result { - 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 input = prepare_yield_input(&self.yielder, input) + .await + .map_err(|e| GatewayError::NetworkError { msg: e.to_string() })?; let fee = self.get_fee(chain, input.clone(), provider.clone()).await?; diff --git a/gemstone/src/gem_yielder/mod.rs b/gemstone/src/gem_yielder/mod.rs index 8d35d0cee..1e11f70e1 100644 --- a/gemstone/src/gem_yielder/mod.rs +++ b/gemstone/src/gem_yielder/mod.rs @@ -6,96 +6,40 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ GemstoneError, alien::{AlienProvider, AlienProviderWrapper}, - models::{GemDelegationBase, GemEarnData, GemEarnType, GemTransactionInputType, GemTransactionLoadInput, GemTransactionLoadMetadata}, + models::{GemEarnData, GemEarnType, GemTransactionInputType, GemTransactionLoadInput, GemTransactionLoadMetadata}, }; use gem_evm::rpc::EthereumClient; use gem_jsonrpc::client::JsonRpcClient; use gem_jsonrpc::rpc::RpcClient; -use primitives::{AssetId, Chain, EVMChain}; -use yielder::{GAS_LIMIT, YO_GATEWAY, YieldDetailsRequest, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; +use primitives::{Chain, EVMChain}; +use yielder::{GAS_LIMIT, YO_GATEWAY, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; -#[derive(uniffi::Object)] -pub struct GemYielder { - yielder: Yielder, -} - -#[uniffi::export] -impl GemYielder { - #[uniffi::constructor] - pub fn new(rpc_provider: Arc) -> Result { - let yielder = build_yielder(rpc_provider)?; - Ok(Self { yielder }) - } - - pub async fn yields_for_asset(&self, asset_id: &AssetId) -> Result, GemstoneError> { - self.yielder.yields_for_asset_with_apy(asset_id).await.map_err(Into::into) - } - - pub async fn deposit(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String, value: String) -> Result { - self.yielder.deposit(provider, &asset, &wallet_address, &value).await.map_err(Into::into) - } - - pub async fn withdraw(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String, value: String) -> Result { - self.yielder.withdraw(provider, &asset, &wallet_address, &value).await.map_err(Into::into) - } - - pub async fn positions(&self, provider: GemYieldProvider, asset: AssetId, wallet_address: String) -> Result { - let request = YieldDetailsRequest { asset_id: asset, wallet_address }; - self.yielder.positions(provider, &request).await.map_err(Into::into) - } - - pub async fn build_transaction( - &self, - action: GemEarnType, - provider: GemYieldProvider, - asset: AssetId, - wallet_address: String, - value: String, - nonce: u64, - chain_id: u64, - ) -> Result { - let transaction = match action { - GemEarnType::Deposit(_) => self.yielder.deposit(provider, &asset, &wallet_address, &value).await?, - GemEarnType::Withdraw(_) => self.yielder.withdraw(provider, &asset, &wallet_address, &value).await?, - }; - - Ok(GemEarnTransactionData { - transaction, - nonce, - chain_id, - gas_limit: GAS_LIMIT.to_string(), - }) - } -} - -pub(crate) fn build_yielder(rpc_provider: Arc) -> Result { +pub(crate) fn build_yielder(rpc_provider: Arc) -> Yielder { 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 build_gateway = |chain: Chain, evm_chain: EVMChain| -> Option<(Chain, Arc)> { + let endpoint = rpc_provider.get_endpoint(chain).ok()?; 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))) + Some((chain, Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY)) as Arc)) }; - let gateways: HashMap> = HashMap::from([ - (Chain::Base, build_gateway(Chain::Base, EVMChain::Base)?), - (Chain::Ethereum, build_gateway(Chain::Ethereum, EVMChain::Ethereum)?), - ]); + let gateways: HashMap> = [build_gateway(Chain::Base, EVMChain::Base), build_gateway(Chain::Ethereum, EVMChain::Ethereum)] + .into_iter() + .flatten() + .collect(); let yo_provider: Arc = Arc::new(YoYieldProvider::new(gateways)); - Ok(Yielder::new(vec![yo_provider])) + Yielder::new(vec![yo_provider]) } pub(crate) async fn prepare_yield_input(yielder: &Yielder, input: GemTransactionLoadInput) -> Result { match (&input.input_type, &input.metadata) { - ( - GemTransactionInputType::Earn { asset, earn_type }, - GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data: None }, - ) => { + (GemTransactionInputType::Earn { asset, earn_type }, GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data: None }) => { + let provider = earn_type.provider_id().parse::().map_err(|e| GemstoneError::from(e.to_string()))?; let transaction = match earn_type { - GemEarnType::Deposit(_) => yielder.deposit(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?, - GemEarnType::Withdraw(_) => yielder.withdraw(YieldProvider::Yo, &asset.id, &input.sender_address, &input.value).await?, + GemEarnType::Deposit(_) => yielder.deposit(provider, &asset.id, &input.sender_address, &input.value).await?, + GemEarnType::Withdraw(_) => yielder.withdraw(provider, &asset.id, &input.sender_address, &input.value).await?, }; Ok(GemTransactionLoadInput { diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs index b0142cd1f..d9bc9841b 100644 --- a/gemstone/src/gem_yielder/remote_types.rs +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -1,7 +1,5 @@ use primitives::{AssetId, YieldProvider}; -use yielder::{Yield, EarnTransaction}; - -use crate::models::swap::GemApprovalData; +use yielder::Yield; pub type GemYieldProvider = YieldProvider; @@ -10,14 +8,6 @@ pub enum GemYieldProvider { Yo, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct GemEarnTransactionData { - pub transaction: GemEarnTransaction, - pub nonce: u64, - pub chain_id: u64, - pub gas_limit: String, -} - pub type GemYield = Yield; #[uniffi::remote(Record)] @@ -27,15 +17,3 @@ pub struct GemYield { pub provider: GemYieldProvider, pub apy: Option, } - -pub type GemEarnTransaction = EarnTransaction; - -#[uniffi::remote(Record)] -pub struct GemEarnTransaction { - pub chain: primitives::Chain, - pub from: String, - pub to: String, - pub data: String, - pub value: Option, - pub approval: Option, -} diff --git a/gemstone/src/models/balance.rs b/gemstone/src/models/balance.rs index 9c3588e4f..0af403cc3 100644 --- a/gemstone/src/models/balance.rs +++ b/gemstone/src/models/balance.rs @@ -22,6 +22,7 @@ pub struct GemBalance { pub pending_unconfirmed: GemBigUint, pub rewards: GemBigUint, pub reserved: GemBigUint, + pub earn: GemBigUint, pub withdrawable: GemBigUint, pub metadata: Option, } From 9a1420d233d9b8ad3f91299752d7d546f2082343 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:02:15 +0200 Subject: [PATCH 41/43] GrowthProviderType -> EarnProviderType --- crates/gem_aptos/src/provider/staking_mapper.rs | 4 ++-- crates/gem_cosmos/src/provider/staking_mapper.rs | 4 ++-- crates/gem_evm/src/provider/preload_mapper.rs | 14 +++++++------- crates/gem_evm/src/provider/staking_ethereum.rs | 4 ++-- crates/gem_evm/src/provider/staking_monad.rs | 4 ++-- crates/gem_evm/src/provider/staking_smartchain.rs | 4 ++-- .../gem_hypercore/src/provider/staking_mapper.rs | 4 ++-- crates/gem_hypercore/src/signer/core_signer.rs | 6 +++--- crates/gem_solana/src/provider/staking_mapper.rs | 4 ++-- crates/gem_sui/src/provider/staking_mapper.rs | 4 ++-- crates/gem_tron/src/provider/preload_mapper.rs | 4 ++-- crates/gem_tron/src/provider/staking_mapper.rs | 6 +++--- crates/primitives/src/delegation.rs | 4 ++-- .../src/{growth_provider.rs => earn_provider.rs} | 2 +- crates/primitives/src/lib.rs | 4 ++-- crates/primitives/src/testkit/delegation_mock.rs | 4 ++-- gemstone/src/models/stake.rs | 8 ++++---- 17 files changed, 42 insertions(+), 42 deletions(-) rename crates/primitives/src/{growth_provider.rs => earn_provider.rs} (95%) diff --git a/crates/gem_aptos/src/provider/staking_mapper.rs b/crates/gem_aptos/src/provider/staking_mapper.rs index 2691623c4..af7aefbb4 100644 --- a/crates/gem_aptos/src/provider/staking_mapper.rs +++ b/crates/gem_aptos/src/provider/staking_mapper.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use num_bigint::BigUint; -use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use crate::models::{DelegationPoolStake, StakingConfig, ValidatorInfo, ValidatorSet}; @@ -21,7 +21,7 @@ pub fn map_validator(validator: &ValidatorInfo, apy: f64, commission: f64, is_ac is_active, commission, apr: apy, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } } diff --git a/crates/gem_cosmos/src/provider/staking_mapper.rs b/crates/gem_cosmos/src/provider/staking_mapper.rs index f70b38bd3..8f5c153c4 100644 --- a/crates/gem_cosmos/src/provider/staking_mapper.rs +++ b/crates/gem_cosmos/src/provider/staking_mapper.rs @@ -11,7 +11,7 @@ use crate::models::{OsmosisDistributionProportions, OsmosisMintParams}; use number_formatter::BigNumberFormatter; use primitives::chain_cosmos::CosmosChain; -use primitives::{DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use std::collections::HashMap; const BOND_STATUS_BONDED: &str = "BOND_STATUS_BONDED"; @@ -68,7 +68,7 @@ pub fn map_staking_validators(validators: Vec, chain: CosmosChain, ap is_active, commission: commission_rate * 100.0, apr: validator_apr, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } }) .collect() diff --git a/crates/gem_evm/src/provider/preload_mapper.rs b/crates/gem_evm/src/provider/preload_mapper.rs index fce6068ed..0f2c6aef0 100644 --- a/crates/gem_evm/src/provider/preload_mapper.rs +++ b/crates/gem_evm/src/provider/preload_mapper.rs @@ -308,7 +308,7 @@ mod tests { use super::*; use crate::everstake::{EVERSTAKE_POOL_ADDRESS, IAccounting}; use num_bigint::BigUint; - use primitives::{Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, RedelegateData}; + use primitives::{Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, RedelegateData}; fn everstake_validator() -> DelegationValidator { DelegationValidator { @@ -318,7 +318,7 @@ mod tests { is_active: true, commission: 10.0, apr: 4.2, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } } @@ -452,7 +452,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }; let stake_type = StakeType::Stake(validator); @@ -489,7 +489,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }, price: None, }; @@ -527,7 +527,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }, price: None, }; @@ -539,7 +539,7 @@ mod tests { is_active: true, commission: 3.0, apr: 12.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }; let redelegate_data = RedelegateData { delegation, to_validator }; @@ -577,7 +577,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }, price: None, }; diff --git a/crates/gem_evm/src/provider/staking_ethereum.rs b/crates/gem_evm/src/provider/staking_ethereum.rs index d4cb93572..77110dcc6 100644 --- a/crates/gem_evm/src/provider/staking_ethereum.rs +++ b/crates/gem_evm/src/provider/staking_ethereum.rs @@ -1,7 +1,7 @@ use gem_client::Client; use num_bigint::BigUint; use num_traits::Zero; -use primitives::{AssetBalance, AssetId, Balance, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{AssetBalance, AssetId, Balance, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use std::error::Error; use crate::everstake::{EVERSTAKE_POOL_ADDRESS, get_everstake_account_state, map_balance_to_delegation, map_withdraw_request_to_delegations}; @@ -32,7 +32,7 @@ impl EthereumClient { is_active: true, commission: 0.1, apr: apy, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }]) } diff --git a/crates/gem_evm/src/provider/staking_monad.rs b/crates/gem_evm/src/provider/staking_monad.rs index 582ea9254..bf048f24d 100644 --- a/crates/gem_evm/src/provider/staking_monad.rs +++ b/crates/gem_evm/src/provider/staking_monad.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; use gem_client::Client; use num_bigint::BigUint; use num_traits::{ToPrimitive, Zero}; -use primitives::{AssetBalance, AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{AssetBalance, AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use crate::monad::{ IMonadStakingLens, MONAD_SCALE, MonadLensBalance, MonadLensDelegation, MonadLensValidatorInfo, STAKING_LENS_CONTRACT, decode_get_lens_apys, decode_get_lens_balance, @@ -124,7 +124,7 @@ impl EthereumClient { is_active: validator.is_active, commission: Self::lens_commission_rate(&validator.commission), apr: if validator.apy_bps > 0 { validator.apy_bps as f64 / 100.0 } else { network_apy }, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } } diff --git a/crates/gem_evm/src/provider/staking_smartchain.rs b/crates/gem_evm/src/provider/staking_smartchain.rs index 6c4459425..4f5921f66 100644 --- a/crates/gem_evm/src/provider/staking_smartchain.rs +++ b/crates/gem_evm/src/provider/staking_smartchain.rs @@ -7,7 +7,7 @@ use gem_bsc::stake_hub::{ }; use gem_client::Client; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use std::{error::Error, str::FromStr}; #[cfg(feature = "rpc")] @@ -37,7 +37,7 @@ impl EthereumClient { is_active: !v.jailed, commission: v.commission as f64 / 10000.0, apr: v.apy as f64 / 100.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }) .collect()) } diff --git a/crates/gem_hypercore/src/provider/staking_mapper.rs b/crates/gem_hypercore/src/provider/staking_mapper.rs index d5b4c3f1d..92a53a4b1 100644 --- a/crates/gem_hypercore/src/provider/staking_mapper.rs +++ b/crates/gem_hypercore/src/provider/staking_mapper.rs @@ -1,7 +1,7 @@ use crate::models::balance::{DelegationBalance, Validator}; use num_bigint::BigUint; use number_formatter::BigNumberFormatter; -use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use std::str::FromStr; pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Option) -> Vec { @@ -15,7 +15,7 @@ pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Opt is_active: x.is_active, commission: x.commission, apr: calculated_apy, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }) .collect() } diff --git a/crates/gem_hypercore/src/signer/core_signer.rs b/crates/gem_hypercore/src/signer/core_signer.rs index 484cd5c06..3c39d5a0c 100644 --- a/crates/gem_hypercore/src/signer/core_signer.rs +++ b/crates/gem_hypercore/src/signer/core_signer.rs @@ -428,7 +428,7 @@ mod tests { use crate::core::actions::Grouping; use num_bigint::{BigInt, BigUint}; use primitives::{ - Asset, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, GasPriceType, StakeType, TransactionInputType, + Asset, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, GasPriceType, StakeType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, }; @@ -443,7 +443,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }; let input = TransactionLoadInput { input_type: TransactionInputType::Stake(asset.clone(), StakeType::Stake(validator)), @@ -498,7 +498,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }, price: None, }; diff --git a/crates/gem_solana/src/provider/staking_mapper.rs b/crates/gem_solana/src/provider/staking_mapper.rs index 9c3f02f7a..20d2ea5e8 100644 --- a/crates/gem_solana/src/provider/staking_mapper.rs +++ b/crates/gem_solana/src/provider/staking_mapper.rs @@ -1,7 +1,7 @@ use crate::models::{EpochInfo, TokenAccountInfo, VoteAccount}; use chrono::Utc; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; pub fn map_staking_validators(vote_accounts: Vec, chain: Chain, network_apy: f64) -> Vec { vote_accounts @@ -18,7 +18,7 @@ pub fn map_staking_validators(vote_accounts: Vec, chain: Chain, net is_active, commission: validator.commission as f64, apr: validator_apr, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } }) .collect() diff --git a/crates/gem_sui/src/provider/staking_mapper.rs b/crates/gem_sui/src/provider/staking_mapper.rs index 0ff96f177..908cf030c 100644 --- a/crates/gem_sui/src/provider/staking_mapper.rs +++ b/crates/gem_sui/src/provider/staking_mapper.rs @@ -2,7 +2,7 @@ use crate::models::RpcSuiSystemState; use crate::models::staking::{SuiStakeDelegation, SuiSystemState, SuiValidators}; use chrono::{DateTime, Utc}; use num_bigint::BigUint; -use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, StakeValidator}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, StakeValidator}; pub fn map_validators(validators: SuiValidators, default_apy: f64) -> Vec { validators @@ -15,7 +15,7 @@ pub fn map_validators(validators: SuiValidators, default_apy: f64) -> Vec ChainParameter { @@ -278,7 +278,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }); let with_bandwidth = account_usage(DEFAULT_BANDWIDTH_BYTES, 0, 0); diff --git a/crates/gem_tron/src/provider/staking_mapper.rs b/crates/gem_tron/src/provider/staking_mapper.rs index 75e94090a..b6fbaf9c3 100644 --- a/crates/gem_tron/src/provider/staking_mapper.rs +++ b/crates/gem_tron/src/provider/staking_mapper.rs @@ -1,6 +1,6 @@ use crate::address::TronAddress; use crate::models::WitnessesList; -use primitives::{Chain, DelegationValidator, GrowthProviderType, StakeValidator}; +use primitives::{Chain, DelegationValidator, EarnProviderType, StakeValidator}; const SYSTEM_UNSTAKING_VALIDATOR_ID: &str = "system"; const SYSTEM_UNSTAKING_VALIDATOR_NAME: &str = "Unstaking"; @@ -22,7 +22,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option) -> Vec is_active: witness.is_jobs.unwrap_or(false), commission: 0.0, apr: default_apy, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }) }) .collect(); @@ -34,7 +34,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option) -> Vec is_active: true, commission: 0.0, apr: default_apy, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, }); validators diff --git a/crates/primitives/src/delegation.rs b/crates/primitives/src/delegation.rs index 410a8212d..b2acff46f 100644 --- a/crates/primitives/src/delegation.rs +++ b/crates/primitives/src/delegation.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use strum::{AsRefStr, Display, EnumString}; use typeshare::typeshare; -use crate::growth_provider::GrowthProviderType; +use crate::earn_provider::EarnProviderType; use crate::{AssetId, Chain, Price, StakeValidator}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,7 +46,7 @@ pub struct DelegationValidator { pub is_active: bool, pub commission: f64, pub apr: f64, - pub provider_type: GrowthProviderType, + pub provider_type: EarnProviderType, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq)] diff --git a/crates/primitives/src/growth_provider.rs b/crates/primitives/src/earn_provider.rs similarity index 95% rename from crates/primitives/src/growth_provider.rs rename to crates/primitives/src/earn_provider.rs index 5ec600bf0..075102dcb 100644 --- a/crates/primitives/src/growth_provider.rs +++ b/crates/primitives/src/earn_provider.rs @@ -6,7 +6,7 @@ use typeshare::typeshare; #[typeshare(swift = "Equatable, CaseIterable, Sendable")] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] -pub enum GrowthProviderType { +pub enum EarnProviderType { Stake, Earn, } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 897e2b4c8..c957eada0 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -221,8 +221,8 @@ pub mod chart; pub use self::chart::{ChartCandleStick, ChartDateValue}; pub mod delegation; pub use self::delegation::{Delegation, DelegationBase, DelegationState, DelegationValidator}; -pub mod growth_provider; -pub use self::growth_provider::{GrowthProviderType, YieldProvider}; +pub mod earn_provider; +pub use self::earn_provider::{EarnProviderType, YieldProvider}; pub mod earn_type; pub use self::earn_type::EarnType; pub mod transaction_update; diff --git a/crates/primitives/src/testkit/delegation_mock.rs b/crates/primitives/src/testkit/delegation_mock.rs index a59f29330..98d63e126 100644 --- a/crates/primitives/src/testkit/delegation_mock.rs +++ b/crates/primitives/src/testkit/delegation_mock.rs @@ -1,4 +1,4 @@ -use crate::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType}; +use crate::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use num_bigint::BigUint; impl Delegation { @@ -56,7 +56,7 @@ impl DelegationValidator { is_active: true, commission: 0.05, apr: 0.08, - provider_type: GrowthProviderType::Stake, + provider_type: EarnProviderType::Stake, } } } diff --git a/gemstone/src/models/stake.rs b/gemstone/src/models/stake.rs index 29fd17a53..cc5d158d6 100644 --- a/gemstone/src/models/stake.rs +++ b/gemstone/src/models/stake.rs @@ -1,6 +1,6 @@ use crate::models::custom_types::{DateTimeUtc, GemBigUint}; use primitives::stake_type::{FreezeType, Resource}; -use primitives::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, GrowthProviderType, Price, StakeChain}; +use primitives::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, Price, StakeChain}; pub type GemFreezeType = FreezeType; pub type GemResource = Resource; @@ -8,7 +8,7 @@ pub type GemDelegation = Delegation; pub type GemDelegationBase = DelegationBase; pub type GemDelegationValidator = DelegationValidator; pub type GemDelegationState = DelegationState; -pub type GemGrowthProviderType = GrowthProviderType; +pub type GemEarnProviderType = EarnProviderType; pub type GemPrice = Price; pub type GemStakeChain = StakeChain; @@ -52,7 +52,7 @@ pub enum GemDelegationState { } #[uniffi::remote(Enum)] -pub enum GemGrowthProviderType { +pub enum GemEarnProviderType { Stake, Earn, } @@ -65,7 +65,7 @@ pub struct GemDelegationValidator { pub is_active: bool, pub commission: f64, pub apr: f64, - pub provider_type: GemGrowthProviderType, + pub provider_type: GemEarnProviderType, } #[uniffi::remote(Record)] From db80bb5fada8b2b6f8eaa3e1919a7dcd06098f2c Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:24:33 +0200 Subject: [PATCH 42/43] Fix merge issues after main merge - Remove duplicate encode_with_0x function in hex.rs - Add missing StakeData import in gemstone transaction models --- crates/primitives/src/hex.rs | 4 ---- gemstone/src/models/transaction.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/primitives/src/hex.rs b/crates/primitives/src/hex.rs index b1b21ecb7..bd0c42f53 100644 --- a/crates/primitives/src/hex.rs +++ b/crates/primitives/src/hex.rs @@ -35,10 +35,6 @@ 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::*; diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index c4561ca07..738ce8dc8 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -2,7 +2,7 @@ use crate::models::*; use num_bigint::BigInt; use primitives::stake_type::FreezeData; use primitives::{ - AccountDataType, Asset, EarnData, EarnType, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, Resource, StakeType, + AccountDataType, Asset, EarnData, EarnType, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, Resource, StakeData, StakeType, TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, UInt64, WalletConnectionSessionAppMetadata, From 4eec89a2d30fb30eaee210dded19fb1d300942aa Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:26:48 +0200 Subject: [PATCH 43/43] Format transaction.rs imports --- gemstone/src/models/transaction.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 738ce8dc8..11b3a8ded 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -2,10 +2,10 @@ use crate::models::*; use num_bigint::BigInt; use primitives::stake_type::FreezeData; use primitives::{ - AccountDataType, Asset, EarnData, EarnType, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, Resource, StakeData, StakeType, - TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, - TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, - UInt64, WalletConnectionSessionAppMetadata, + AccountDataType, Asset, EarnData, EarnType, FeeOption, GasPriceType, HyperliquidOrder, PerpetualConfirmData, PerpetualDirection, PerpetualProvider, PerpetualType, Resource, + StakeData, StakeType, TransactionChange, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransactionMetadata, + TransactionPerpetualMetadata, TransactionState, TransactionStateRequest, TransactionType, TransactionUpdate, TransferDataExtra, TransferDataOutputAction, + TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, UInt64, WalletConnectionSessionAppMetadata, perpetual::{CancelOrderData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, TPSLOrderData}, }; use std::collections::HashMap;