diff --git a/AGENTS.md b/AGENTS.md index 16a3fb3e7..7b1ca743a 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) @@ -201,24 +203,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 ### Code Organization - **Modular structure**: Break down long files into smaller, focused modules by logical responsibility @@ -284,7 +280,6 @@ Direct repository access methods available on `DatabaseClient` include: - **Use `primitives::hex`** for hex encoding/decoding (not `alloy_primitives::hex`) - 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 @@ -295,8 +290,7 @@ 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 diff --git a/Cargo.lock b/Cargo.lock index 31c73d164..698dc5085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3657,11 +3657,13 @@ dependencies = [ "serde", "serde_json", "signer", + "strum", "sui-sdk-types", "swapper", "tokio", "uniffi", "url", + "yielder", "zeroize", ] @@ -6515,9 +6517,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", @@ -9519,6 +9521,27 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "yielder" +version = "1.0.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "async-trait", + "futures", + "gem_client", + "gem_evm", + "gem_jsonrpc", + "num-bigint", + "primitives", + "reqwest 0.13.1", + "serde", + "serde_json", + "serde_serializers", + "strum", + "tokio", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index f765b08f7..00f3772ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "crates/streamer", "crates/swapper", "crates/tracing", + "crates/yielder", ] [workspace.dependencies] diff --git a/apps/daemon/src/pusher/pusher.rs b/apps/daemon/src/pusher/pusher.rs index 2bdb07d72..6560a5edf 100644 --- a/apps/daemon/src/pusher/pusher.rs +++ b/apps/daemon/src/pusher/pusher.rs @@ -120,6 +120,7 @@ impl Pusher { title: localizer.notification_unfreeze_title(self.get_value(amount, asset.symbol.clone()).as_str()), message: None, }), + TransactionType::EarnDeposit | TransactionType::EarnWithdraw => Err("Earn transactions not implemented".into()), } } 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 0f38bd35c..f252ff43a 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -20,8 +20,8 @@ use crate::{ jito::{JitoClient, JitoTipFloor}, solana_client::{JUPITER_PROGRAM, SolanaFeeData, SolanaGasClient}, }; +use gem_jsonrpc::native_provider::NativeProvider; use gem_evm::ether_conv::EtherConv; -use gemstone::alien::reqwest_provider::NativeProvider; use primitives::fee::FeePriority; #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] diff --git a/bin/gas-bench/src/solana_client.rs b/bin/gas-bench/src/solana_client.rs index 126b05abe..175ddf07f 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, calculate_fee_stats}; 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; diff --git a/crates/gem_aptos/src/provider/staking_mapper.rs b/crates/gem_aptos/src/provider/staking_mapper.rs index b086579a9..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}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; 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: EarnProviderType::Stake, } } diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index 30282aac0..71feec072 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -107,7 +107,11 @@ impl AptosClient { AssetSubtype::TOKEN => Ok(1500), } } - TransactionInputType::Swap(_, _, _) | TransactionInputType::Stake(_, _) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) => Ok(1500), + TransactionInputType::Swap(_, _, _) + | TransactionInputType::Stake(_, _) + | TransactionInputType::TokenApprove(_, _) + | TransactionInputType::Generic(_, _, _) + | 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 3a9913803..5c4cb45d7 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::Earn(_, _) => 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::Earn(_, _) => 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::Earn(_, _) => 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::Earn(_, _) => 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::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), }, @@ -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::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..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}; +use primitives::{DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; 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: EarnProviderType::Stake, } }) .collect() diff --git a/crates/gem_evm/src/call_decoder.rs b/crates/gem_evm/src/call_decoder.rs index 36e9fca86..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()); } @@ -108,6 +105,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![("owner", "address", allowance.owner.to_string()), ("spender", "address", allowance.spender.to_string())], @@ -148,7 +146,6 @@ mod tests { #[test] fn test_decode_custom_abi() { - // Using ERC721 safeTransferFrom as test case let calldata = "0x42842e0e0000000000000000000000008ba1f109551bd432803012645aac136c0c3def25000000000000000000000000271682deb8c4e0901d1a1550ad2e64d568e69909000000000000000000000000000000000000000000000000000000000000007b"; let abi = r#"[ { @@ -190,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/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/everstake/client.rs b/crates/gem_evm/src/everstake/client.rs index b23fd19d6..c09b430b3 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 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::(&deposited) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let pending_balance = results + .decode::(&pending) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let pending_deposited_balance = results + .decode::(&pending_deposited) + .map(u256_to_biguint) + .unwrap_or_else(|_| BigUint::zero()); + let withdraw_request = results.decode::(&withdraw)?; + let restaked_reward = results + .decode::(&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/lib.rs b/crates/gem_evm/src/lib.rs index cdb1e8e1b..4abe729c6 100644 --- a/crates/gem_evm/src/lib.rs +++ b/crates/gem_evm/src/lib.rs @@ -15,6 +15,7 @@ pub mod fee_calculator; pub mod jsonrpc; pub mod message; pub mod monad; +#[cfg(feature = "rpc")] pub mod multicall3; pub mod permit2; #[cfg(feature = "rpc")] diff --git a/crates/gem_evm/src/multicall3.rs b/crates/gem_evm/src/multicall3.rs index 89e4a9a8e..9e2b6c398 100644 --- a/crates/gem_evm/src/multicall3.rs +++ b/crates/gem_evm/src/multicall3.rs @@ -1,49 +1,130 @@ +use std::{fmt, marker::PhantomData}; + +use alloy_primitives::Address; use alloy_sol_types::{SolCall, sol}; -use primitives::EVMChain; +use gem_client::Client; +use primitives::chain_config::ChainStack; +use primitives::hex; +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); + } +} + +pub struct CallHandle { + index: usize, + _marker: PhantomData, +} + +pub struct Multicall3Results { + results: Vec, +} + +impl Multicall3Results { + pub fn decode(&self, handle: &CallHandle) -> Result { + let result = self.results.get(handle.index).ok_or_else(|| Multicall3Error(format!("invalid index: {}", handle.index)))?; - function aggregate3Value(Call3Value[] calldata calls) - external - payable - returns (Result[] memory returnData); + if !result.success { + return Err(Multicall3Error(format!("{} failed", T::SIGNATURE))); + } - function tryAggregate(bool requireSuccess, Call[] calldata calls) - external - payable - returns (Result[] memory returnData); + T::abi_decode_returns(&result.returnData).map_err(|e| Multicall3Error(format!("{}: {:?}", T::SIGNATURE, e))) + } +} + +pub struct Multicall3Builder<'a, C: Client + Clone> { + client: &'a EthereumClient, + calls: Vec, + block: Option, +} + +impl<'a, C: Client + Clone> Multicall3Builder<'a, C> { + pub fn new(client: &'a EthereumClient) -> Self { + Self { + client, + calls: Vec::new(), + block: None, + } + } + + 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 } + } + + pub fn at_block(mut self, block: u64) -> Self { + self.block = Some(block); + self + } + + pub async fn execute(self) -> Result { + if self.calls.is_empty() { + return Ok(Multicall3Results { results: vec![] }); + } + + 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")); + + let result: String = self + .client + .client + .call( + "eth_call", + json!([{ + "to": address, + "data": hex::encode_with_0x(&multicall_data) + }, block_param]), + ) + .await + .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()))?; + + 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", } } @@ -55,42 +136,32 @@ 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()) + Err(format!("{} failed", T::SIGNATURE)) } } -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", +#[cfg(test)] +mod tests { + use super::*; + use crate::contracts::IERC20; + use alloy_primitives::U256; + + #[test] + fn test_multicall3_results_decode_success() { + let value = U256::from(42u64); + let handle = CallHandle { index: 0, _marker: PhantomData }; + let results = Multicall3Results { + results: vec![IMulticall3::Result { + success: true, + returnData: value.to_be_bytes::<32>().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 1487aa921..430ec1cf0 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; @@ -63,20 +61,17 @@ 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(_, _) => match input.metadata { 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), - }), + earn_data: Some(EarnData::stake(params.to, ¶ms.data)), }, _ => input.metadata, - } - } else { - input.metadata + }, + TransactionInputType::Earn(_, _) => 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 17d167e3c..0f2c6aef0 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; @@ -37,7 +37,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> { @@ -45,7 +45,7 @@ pub fn map_transaction_preload(nonce_hex: String, chain_id: String) -> Result()?, - stake_data: None, + earn_data: None, }) } @@ -92,15 +92,11 @@ 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())?, - 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)?)?; @@ -145,6 +141,20 @@ pub fn get_transaction_params(chain: EVMChain, input: &TransactionLoadInput) -> } _ => Err("Unsupported chain for staking".into()), }, + 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)?; + Ok(TransactionParams::new(contract_address.clone(), decoded_data, BigInt::from(0))) + } + } _ => Err("Unsupported transfer type".into()), } } @@ -183,6 +193,20 @@ pub fn get_extra_fee_gas_limit(input: &TransactionLoadInput) -> Result { + 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 { + Ok(BigInt::from(0)) + } + } _ => Ok(BigInt::from(0)), } } @@ -284,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, EarnProviderType, RedelegateData}; fn everstake_validator() -> DelegationValidator { DelegationValidator { @@ -294,6 +318,7 @@ mod tests { is_active: true, commission: 10.0, apr: 4.2, + provider_type: EarnProviderType::Stake, } } @@ -305,10 +330,10 @@ 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, earn_data } => { assert_eq!(nonce, 10); assert_eq!(chain_id, 1); - assert!(stake_data.is_none()); + assert!(earn_data.is_none()); } _ => panic!("Expected Evm variant"), } @@ -379,7 +404,6 @@ mod tests { let result = map_transaction_fee_rates(EVMChain::SmartChain, &fee_history)?; 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); @@ -428,6 +452,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: EarnProviderType::Stake, }; let stake_type = StakeType::Stake(validator); @@ -464,6 +489,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: EarnProviderType::Stake, }, price: None, }; @@ -501,6 +527,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + provider_type: EarnProviderType::Stake, }, price: None, }; @@ -512,6 +539,7 @@ mod tests { is_active: true, commission: 3.0, apr: 12.0, + provider_type: EarnProviderType::Stake, }; let redelegate_data = RedelegateData { delegation, to_validator }; @@ -549,6 +577,7 @@ mod tests { is_active: true, commission: 5.0, apr: 10.0, + 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 6111a1646..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}; +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,6 +32,7 @@ impl EthereumClient { is_active: true, commission: 0.1, apr: apy, + 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 8a7e622ed..4fa6bb661 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, EarnProviderType}; 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: EarnProviderType::Stake, } } diff --git a/crates/gem_evm/src/provider/staking_smartchain.rs b/crates/gem_evm/src/provider/staking_smartchain.rs index 66f6a4b9a..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}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; 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: EarnProviderType::Stake, }) .collect()) } diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index bf87c83d7..79c597c26 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -2,28 +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; #[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}; +use crate::multicall3::{IMulticall3, Multicall3Builder, deployment_by_chain_stack}; pub const FUNCTION_ERC20_NAME: &str = "0x06fdde03"; pub const FUNCTION_ERC20_SYMBOL: &str = "0x95d89b41"; @@ -254,22 +245,30 @@ impl EthereumClient { } #[cfg(feature = "rpc")] - pub async fn multicall3(&self, calls: Vec) -> Result, Box> { - let multicall_address = deployment_by_chain(&self.chain); + pub fn multicall(&self) -> Multicall3Builder<'_, C> { + Multicall3Builder::new(self) + } + + #[cfg(feature = "rpc")] + pub async fn multicall3(&self, calls: Vec) -> Result, Box> { + use alloy_sol_types::SolCall; + + let multicall_address = deployment_by_chain_stack(self.chain.chain_stack()); 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 + .client + .call( + "eth_call", + json!([{ + "to": multicall_address, + "data": hex::encode_prefixed(&multicall_data) + }, "latest"]), + ) + .await?; - 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)?; - - Ok(multicall_results) + let results = IMulticall3::aggregate3Call::abi_decode_returns(&result_data)?; + Ok(results) } } 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/gem_hypercore/src/provider/staking_mapper.rs b/crates/gem_hypercore/src/provider/staking_mapper.rs index 41fca6c59..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}; +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,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: EarnProviderType::Stake, }) .collect() } diff --git a/crates/gem_hypercore/src/signer/core_signer.rs b/crates/gem_hypercore/src/signer/core_signer.rs index 32ecacf33..3c39d5a0c 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, EarnProviderType, GasPriceType, StakeType, TransactionInputType, + TransactionLoadInput, TransactionLoadMetadata, }; #[test] @@ -443,6 +443,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, + provider_type: EarnProviderType::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: EarnProviderType::Stake, }, price: None, }; 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..de41aa5ff 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 => self.client.request(reqwest::Method::OPTIONS, target.url), }; + 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/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index e2e1c0080..d452423db 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::Earn(_, _) => 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::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..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}; +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,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: EarnProviderType::Stake, } }) .collect() diff --git a/crates/gem_sui/src/provider/preload_mapper.rs b/crates/gem_sui/src/provider/preload_mapper.rs index a4263a7b9..b04aa7d29 100644 --- a/crates/gem_sui/src/provider/preload_mapper.rs +++ b/crates/gem_sui/src/provider/preload_mapper.rs @@ -36,7 +36,8 @@ fn get_gas_limit(input_type: &TransactionInputType) -> u64 { | TransactionInputType::Deposit(_) | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) - | TransactionInputType::Perpetual(_, _) => GAS_BUDGET, + | TransactionInputType::Perpetual(_, _) + | 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..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, StakeValidator}; +use primitives::{Chain, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, 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 ChainParameter { @@ -277,6 +278,7 @@ mod tests { is_active: true, commission: 0.0, apr: 0.0, + 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 6b582fa35..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, StakeValidator}; +use primitives::{Chain, DelegationValidator, EarnProviderType, 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: EarnProviderType::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: EarnProviderType::Stake, }); validators 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/primitives/src/asset_metadata.rs b/crates/primitives/src/asset_metadata.rs index fcc63b3f9..8e331f046 100644 --- a/crates/primitives/src/asset_metadata.rs +++ b/crates/primitives/src/asset_metadata.rs @@ -1,23 +1,20 @@ +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 staking_apr: Option, + pub is_earn_enabled: bool, + pub earn_apr: Option, + pub is_pinned: bool, + pub is_active: bool, + pub rank_score: i32, } 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, } diff --git a/crates/primitives/src/banner.rs b/crates/primitives/src/banner.rs index a87453d21..aca9360e7 100644 --- a/crates/primitives/src/banner.rs +++ b/crates/primitives/src/banner.rs @@ -19,6 +19,7 @@ pub enum BannerEvent { SuspiciousAsset, Onboarding, TradePerpetuals, + Earn, } #[typeshare(swift = "Equatable, CaseIterable, Sendable")] diff --git a/crates/primitives/src/delegation.rs b/crates/primitives/src/delegation.rs index e6a3c2357..b2acff46f 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::earn_provider::EarnProviderType; 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: EarnProviderType, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq)] 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_provider.rs b/crates/primitives/src/earn_provider.rs new file mode 100644 index 000000000..075102dcb --- /dev/null +++ b/crates/primitives/src/earn_provider.rs @@ -0,0 +1,20 @@ +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 EarnProviderType { + 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/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/hex.rs b/crates/primitives/src/hex.rs index 35dbe7e24..bd0c42f53 100644 --- a/crates/primitives/src/hex.rs +++ b/crates/primitives/src/hex.rs @@ -50,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/lib.rs b/crates/primitives/src/lib.rs index c9225725d..c147d2ba2 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -63,6 +63,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; @@ -220,6 +221,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::{EarnProviderType, 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; @@ -234,6 +239,9 @@ 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 use self::earn_type::EarnTransaction; +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/swap/approval.rs b/crates/primitives/src/swap/approval.rs index f351741a5..0860c20b0 100644 --- a/crates/primitives/src/swap/approval.rs +++ b/crates/primitives/src/swap/approval.rs @@ -3,7 +3,7 @@ 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 { diff --git a/crates/primitives/src/testkit/delegation_mock.rs b/crates/primitives/src/testkit/delegation_mock.rs index 263a59666..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}; +use crate::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType}; use num_bigint::BigUint; impl Delegation { @@ -56,6 +56,7 @@ impl DelegationValidator { is_active: true, commission: 0.05, apr: 0.08, + provider_type: EarnProviderType::Stake, } } } diff --git a/crates/primitives/src/transaction.rs b/crates/primitives/src/transaction.rs index fbdcb572b..e7d9cacc2 100644 --- a/crates/primitives/src/transaction.rs +++ b/crates/primitives/src/transaction.rs @@ -260,7 +260,9 @@ impl Transaction { | TransactionType::SmartContractCall | TransactionType::PerpetualOpenPosition | TransactionType::PerpetualClosePosition - | TransactionType::PerpetualModifyPosition => vec![self.asset_id.clone(), self.fee_asset_id.clone()], + | TransactionType::PerpetualModifyPosition + | TransactionType::EarnDeposit + | TransactionType::EarnWithdraw => vec![self.asset_id.clone(), self.fee_asset_id.clone()], TransactionType::Swap => self .metadata .clone() @@ -295,7 +297,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::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 b519a4d0d..1a677b78c 100644 --- a/crates/primitives/src/transaction_input_type.rs +++ b/crates/primitives/src/transaction_input_type.rs @@ -1,3 +1,4 @@ +use crate::earn_type::EarnType; use crate::stake_type::StakeType; use crate::swap::{ApprovalData, SwapData}; use crate::transaction_fee::TransactionFee; @@ -21,6 +22,7 @@ pub enum TransactionInputType { TransferNft(Asset, NFTAsset), Account(Asset, AccountDataType), Perpetual(Asset, PerpetualType), + Earn(Asset, EarnType), } impl TransactionInputType { @@ -35,6 +37,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, + TransactionInputType::Earn(asset, _) => asset, } } @@ -63,6 +66,7 @@ impl TransactionInputType { TransactionInputType::TransferNft(asset, _) => asset, TransactionInputType::Account(asset, _) => asset, TransactionInputType::Perpetual(asset, _) => asset, + TransactionInputType::Earn(asset, _) => asset, } } @@ -87,6 +91,10 @@ impl TransactionInputType { PerpetualType::Close(_) | PerpetualType::Reduce(_) => TransactionType::PerpetualClosePosition, PerpetualType::Modify(_) => TransactionType::PerpetualModifyPosition, }, + 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 c65bdfde7..33f77acab 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,10 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - UTXO, - 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 { @@ -48,7 +44,7 @@ pub enum TransactionLoadMetadata { Evm { nonce: u64, chain_id: u64, - stake_data: Option, + earn_data: Option, }, Near { sequence: u64, diff --git a/crates/primitives/src/transaction_type.rs b/crates/primitives/src/transaction_type.rs index 025d0e20f..0eefd012f 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, + EarnDeposit, + EarnWithdraw, } impl TransactionType { 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/approval/evm.rs b/crates/swapper/src/approval/evm.rs index e168267e3..532886271 100644 --- a/crates/swapper/src/approval/evm.rs +++ b/crates/swapper/src/approval/evm.rs @@ -221,7 +221,7 @@ mod tests { ApprovalType::Approve(ApprovalData { token: token.clone(), spender: permit2_contract.clone(), - value: amount.to_string() + value: amount.to_string(), }), ApprovalType::Permit2(Permit2ApprovalData { token: token.clone(), diff --git a/crates/swapper/src/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs index ecf8ec08e..e4740fcca 100644 --- a/crates/swapper/src/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -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 3c3ce2298..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}")) 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 323e240ca..60216a857 100644 --- a/crates/swapper/src/jupiter/provider.rs +++ b/crates/swapper/src/jupiter/provider.rs @@ -199,7 +199,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 582bce7bc..a74754e2a 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -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 c4c023e13..ec3c87f45 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -230,7 +230,7 @@ impl GemSwapper { } } -#[cfg(all(test, feature = "reqwest_provider"))] +#[cfg(test)] mod tests { use std::{borrow::Cow, collections::BTreeSet, sync::Arc, vec}; @@ -240,11 +240,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 { 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 cdaf4c364..f462e0b9b 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,9 @@ pub(crate) mod model; mod provider; mod quote_data_mapper; -use num_bigint::BigInt; +use bigint::value_to; use primitives::Chain; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use crate::alien::RpcProvider; use gem_client::Client; @@ -46,20 +47,6 @@ 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) } @@ -67,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, } diff --git a/crates/swapper/src/thorchain/provider.rs b/crates/swapper/src/thorchain/provider.rs index 05c4d7dd8..1f55f6a0e 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?; @@ -85,7 +87,7 @@ where .await .map_err(|e| self.map_quote_error(e, from_asset.decimals as i32))?; - 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(), @@ -183,7 +185,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] @@ -263,4 +266,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/Cargo.toml b/crates/yielder/Cargo.toml new file mode 100644 index 000000000..a044e9b78 --- /dev/null +++ b/crates/yielder/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "yielder" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +yield_integration_tests = [ + "gem_jsonrpc/reqwest", + "gem_client/reqwest", + "tokio/rt-multi-thread", +] + +[dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +num-bigint = { workspace = true } +gem_client = { path = "../gem_client" } +gem_evm = { path = "../gem_evm", features = ["rpc"] } +gem_jsonrpc = { path = "../gem_jsonrpc" } +primitives = { path = "../primitives" } +serde_serializers = { path = "../serde_serializers" } +async-trait = { workspace = true } +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +strum = { workspace = true } + +[dev-dependencies] +gem_client = { path = "../gem_client", features = ["reqwest"] } +gem_jsonrpc = { path = "../gem_jsonrpc", features = ["reqwest"] } +reqwest = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs new file mode 100644 index 000000000..2d973648f --- /dev/null +++ b/crates/yielder/src/lib.rs @@ -0,0 +1,10 @@ +mod models; +mod provider; +pub mod yo; + +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 new file mode 100644 index 000000000..2149b77c7 --- /dev/null +++ b/crates/yielder/src/models.rs @@ -0,0 +1,28 @@ +use primitives::{AssetId, YieldProvider}; + +pub use primitives::EarnTransaction; + +#[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 YieldDetailsRequest { + pub asset_id: AssetId, + pub wallet_address: String, +} diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs new file mode 100644 index 000000000..7118fff54 --- /dev/null +++ b/crates/yielder/src/provider.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use primitives::{AssetId, Chain, DelegationBase, YieldProvider}; + +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; + async fn yields_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { + Ok(self.yields(asset_id)) + } +} + +pub struct Yielder { + providers: Vec>, +} + +impl Yielder { + pub fn new(providers: Vec>) -> Self { + Self { providers } + } + + pub async fn yields_for_asset_with_apy(&self, asset_id: &AssetId) -> Result, YieldError> { + let mut yields = Vec::new(); + 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.name.cmp(&b.name)) + }); + Ok(yields) + } + + 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 { + 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.get_provider(provider)?; + 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() + .find(|candidate| candidate.provider() == provider) + .cloned() + .ok_or_else(|| format!("provider {provider} not found").into()) + } +} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs new file mode 100644 index 000000000..8e1343e44 --- /dev/null +++ b/crates/yielder/src/yo/client.rs @@ -0,0 +1,160 @@ +use alloy_primitives::hex::{self, encode_prefixed}; +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 primitives::swap::ApprovalData; + +use super::YoVault; +use super::contract::{IYoGateway, IYoVaultToken}; +use super::error::YieldError; +use super::model::PositionData; + +#[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; + 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; +} + +#[derive(Debug, Clone)] +pub struct YoGatewayClient { + ethereum_client: EthereumClient, + contract_address: Address, +} + +impl YoGatewayClient { + pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { + Self { + ethereum_client, + contract_address, + } + } + + fn deposit_call_data(yo_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() + } + + 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() + } + + 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] +impl YoProvider for YoGatewayClient +where + C: Client + Clone + Send + Sync + 'static, +{ + fn contract_address(&self) -> Address { + 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 { + 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 { + 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 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 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_call = latest_batch.add(vault.yo_token, IYoVaultToken::convertToAssetsCall { shares: one_share }); + let latest_ts = latest_batch.add(multicall_addr, IMulticall3::getCurrentBlockTimestampCall {}); + + let latest = latest_batch.at_block(latest_block).execute().await?; + + 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::(); + + 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, + asset_balance, + latest_price, + latest_timestamp, + lookback_price, + 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(), + })) + } else { + Ok(None) + } + } + + async fn convert_to_shares(&self, yo_vault: Address, assets: U256) -> Result { + 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| 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/contract.rs b/crates/yielder/src/yo/contract.rs new file mode 100644 index 000000000..cb9c12ab9 --- /dev/null +++ b/crates/yielder/src/yo/contract.rs @@ -0,0 +1,41 @@ +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); + + 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..94e3529c2 --- /dev/null +++ b/crates/yielder/src/yo/error.rs @@ -0,0 +1,50 @@ +use std::{error::Error, fmt}; + +use gem_evm::multicall3::Multicall3Error; + +pub type BoxError = Box; + +#[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) + } +} + +impl From for YieldError { + fn from(e: Multicall3Error) -> Self { + 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 new file mode 100644 index 000000000..36b229022 --- /dev/null +++ b/crates/yielder/src/yo/mod.rs @@ -0,0 +1,18 @@ +mod client; +mod contract; +mod error; +mod model; +mod provider; +mod vault; + +pub use client::{YoGatewayClient, YoProvider}; +pub use contract::{IYoGateway, IYoVaultToken}; +pub use error::{BoxError, YieldError}; +pub use model::PositionData; +pub use provider::{GAS_LIMIT, YoYieldProvider}; +pub use vault::{YO_USDC, YO_USDT, YoVault, vaults}; + +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/model.rs b/crates/yielder/src/yo/model.rs new file mode 100644 index 000000000..1497a9402 --- /dev/null +++ b/crates/yielder/src/yo/model.rs @@ -0,0 +1,37 @@ +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)] +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, +} + +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 new file mode 100644 index 000000000..567cc48d5 --- /dev/null +++ b/crates/yielder/src/yo/provider.rs @@ -0,0 +1,144 @@ +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::BigUint; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, YieldProvider, swap::ApprovalData}; + +use crate::models::{EarnTransaction, Yield, YieldDetailsRequest}; +use crate::provider::YieldProviderClient; + +use super::{YO_PARTNER_ID_GEM, YoVault, client::YoProvider, error::YieldError, vaults}; + +pub const GAS_LIMIT: &str = "300000"; + +fn lookback_blocks_for_chain(chain: Chain) -> u64 { + match chain { + Chain::Base => 7 * 24 * 60 * 60 / 2, + Chain::Ethereum => 7 * 24 * 60 * 60 / 12, + _ => 7 * 24 * 60 * 60 / 12, + } +} + +pub struct YoYieldProvider { + vaults: Vec, + gateways: HashMap>, +} + +impl YoYieldProvider { + pub fn new(gateways: HashMap>) -> Self { + Self { + vaults: vaults().to_vec(), + gateways, + } + } + + 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 + '_ { + 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(|| 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 { + fn provider(&self) -> YieldProvider { + YieldProvider::Yo + } + + 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)) + .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) { + let apy = self.fetch_vault_apy(vault).await.ok(); + results.push(Yield::new(vault.name, vault.asset_id(), self.provider(), apy)); + } + Ok(results) + } + + 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}"))?; + 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); + Ok(convert_transaction(vault, tx, approval)) + } + + 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}"))?; + 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?; + 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.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?; + + 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 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(), + 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) -> EarnTransaction { + EarnTransaction { + chain: vault.chain, + from: tx.from.unwrap_or_default(), + to: tx.to, + data: tx.data, + value: tx.value, + approval, + } +} diff --git a/crates/yielder/src/yo/vault.rs b/crates/yielder/src/yo/vault.rs new file mode 100644 index 000000000..57f790692 --- /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_USDC: YoVault = YoVault::new( + "yoUSDC", + Chain::Base, + address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), + address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), + 6, +); + +pub const YO_USDT: YoVault = YoVault::new( + "yoUSDT", + Chain::Ethereum, + address!("0xb9a7da9e90d3b428083bae04b860faa6325b721e"), + address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), + 6, +); + +pub fn vaults() -> &'static [YoVault] { + &[YO_USDC, YO_USDT] +} diff --git a/crates/yielder/tests/integration_test.rs b/crates/yielder/tests/integration_test.rs new file mode 100644 index 000000000..57380ffa1 --- /dev/null +++ b/crates/yielder/tests/integration_test.rs @@ -0,0 +1,80 @@ +#![cfg(feature = "yield_integration_tests")] + +use std::{collections::HashMap, sync::Arc}; + +use gem_evm::rpc::EthereumClient; +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}; + +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), + ]) +} + +#[tokio::test] +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_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 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"); + + 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() -> 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 { + asset_id: YO_USDC.asset_id(), + wallet_address: wallet_address.to_string(), + }; + + let position = yo_provider.positions(&request).await?; + println!( + "yielder: position vault_balance={:?} asset_balance={:?}", + position.vault_balance_value, position.asset_balance_value + ); + + assert!(position.vault_balance_value >= BigInt::from(0)); + assert!(position.asset_balance_value >= BigInt::from(0)); + + Ok(()) +} diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 496bfc3fe..7b02aca9b 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -14,11 +14,12 @@ name = "gemstone" [features] default = [] -reqwest_provider = ["dep:reqwest", "swapper/reqwest_provider"] +reqwest_provider = ["dep:reqwest", "gem_jsonrpc/reqwest"] 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", "signer"] } @@ -62,6 +63,7 @@ futures.workspace = true bs58 = { workspace = true } url = { workspace = true } zeroize = { workspace = true } +strum = { workspace = true } [build-dependencies] uniffi = { workspace = true, features = ["build"] } 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 { 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 f4a02e518..fdd089f52 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::{GemYield, build_yielder, prepare_yield_input}; use crate::models::*; use crate::network::JsonRpcClient; use chain_traits::ChainTraits; @@ -32,7 +33,11 @@ use gem_xrp::rpc::client::XRPClient; use std::future::Future; 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)] #[async_trait::async_trait] @@ -47,6 +52,7 @@ pub struct GemGateway { pub preferences: Arc, pub secure_preferences: Arc, pub api_client: GemApiClient, + yielder: Yielder, } impl std::fmt::Debug for GemGateway { @@ -151,11 +157,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()); Self { provider, preferences, secure_preferences, api_client, + yielder, } } @@ -172,6 +180,30 @@ impl GemGateway { self.with_provider(chain, |provider| async move { provider.get_balance_staking(address).await }).await } + 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> { self.with_provider(chain, |provider| async move { provider.get_staking_validators(apy).await }).await } @@ -259,6 +291,10 @@ impl GemGateway { } pub async fn get_transaction_load(&self, chain: Chain, input: GemTransactionLoadInput, provider: Arc) -> Result { + 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?; let load_data = self @@ -318,7 +354,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 new file mode 100644 index 000000000..1e11f70e1 --- /dev/null +++ b/gemstone/src/gem_yielder/mod.rs @@ -0,0 +1,68 @@ +mod remote_types; +pub use remote_types::*; + +use std::{collections::HashMap, sync::Arc}; + +use crate::{ + GemstoneError, + alien::{AlienProvider, AlienProviderWrapper}, + models::{GemEarnData, GemEarnType, GemTransactionInputType, GemTransactionLoadInput, GemTransactionLoadMetadata}, +}; +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use gem_jsonrpc::rpc::RpcClient; +use primitives::{Chain, EVMChain}; +use yielder::{GAS_LIMIT, YO_GATEWAY, YieldProvider, YieldProviderClient, Yielder, YoGatewayClient, YoProvider, YoYieldProvider}; + +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| -> 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); + Some((chain, Arc::new(YoGatewayClient::new(ethereum_client, YO_GATEWAY)) as Arc)) + }; + + 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)); + 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 }) => { + let provider = earn_type.provider_id().parse::().map_err(|e| GemstoneError::from(e.to_string()))?; + let transaction = match earn_type { + 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 { + 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), + } +} diff --git a/gemstone/src/gem_yielder/remote_types.rs b/gemstone/src/gem_yielder/remote_types.rs new file mode 100644 index 000000000..d9bc9841b --- /dev/null +++ b/gemstone/src/gem_yielder/remote_types.rs @@ -0,0 +1,19 @@ +use primitives::{AssetId, YieldProvider}; +use yielder::Yield; + +pub type GemYieldProvider = YieldProvider; + +#[uniffi::remote(Enum)] +pub enum GemYieldProvider { + Yo, +} + +pub type GemYield = Yield; + +#[uniffi::remote(Record)] +pub struct GemYield { + pub name: String, + pub asset_id: AssetId, + pub provider: GemYieldProvider, + pub apy: Option, +} diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index 3fef0361a..18fd2e5c6 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; @@ -16,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"); @@ -106,3 +108,20 @@ impl From for GemstoneError { Self::AnyError { msg: error.to_string() } } } +impl From for GemstoneError { + fn from(error: YieldError) -> Self { + Self::AnyError { msg: error.to_string() } + } +} + +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/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, } 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/stake.rs b/gemstone/src/models/stake.rs index 641b98bd9..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, Price, StakeChain}; +use primitives::{AssetId, Chain, Delegation, DelegationBase, DelegationState, DelegationValidator, EarnProviderType, 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 GemEarnProviderType = EarnProviderType; pub type GemPrice = Price; pub type GemStakeChain = StakeChain; @@ -50,6 +51,12 @@ pub enum GemDelegationState { AwaitingWithdrawal, } +#[uniffi::remote(Enum)] +pub enum GemEarnProviderType { + 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: GemEarnProviderType, } #[uniffi::remote(Record)] diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 40fb4f9c9..11b3a8ded 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, Resource, 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; @@ -127,6 +127,8 @@ pub enum TransactionType { PerpetualOpenPosition, PerpetualClosePosition, PerpetualModifyPosition, + EarnDeposit, + EarnWithdraw, } pub type GemAccountDataType = AccountDataType; @@ -263,6 +265,25 @@ pub enum PerpetualType { Reduce(PerpetualReduceData), } +pub type GemEarnData = EarnData; + +#[uniffi::remote(Record)] +pub struct EarnData { + pub provider: Option, + pub contract_address: Option, + pub call_data: Option, + pub approval: Option, + 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 { @@ -302,6 +323,10 @@ pub enum GemTransactionInputType { asset: GemAsset, perpetual_type: GemPerpetualType, }, + Earn { + asset: GemAsset, + earn_type: GemEarnType, + }, } impl GemTransactionInputType { @@ -314,7 +339,8 @@ impl GemTransactionInputType { | Self::Generic { asset, .. } | Self::TransferNft { asset, .. } | Self::Account { asset, .. } - | Self::Perpetual { asset, .. } => asset, + | Self::Perpetual { asset, .. } + | Self::Earn { asset, .. } => asset, Self::Swap { from_asset, .. } => from_asset, } } @@ -404,7 +430,7 @@ pub enum GemTransactionLoadMetadata { Evm { nonce: u64, chain_id: u64, - stake_data: Option, + earn_data: Option, }, Near { sequence: u64, @@ -489,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, earn_data } => GemTransactionLoadMetadata::Evm { nonce, chain_id, earn_data }, TransactionLoadMetadata::Near { sequence, block_hash } => GemTransactionLoadMetadata::Near { sequence, block_hash }, TransactionLoadMetadata::Stellar { sequence, @@ -577,7 +603,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, earn_data } => TransactionLoadMetadata::Evm { nonce, chain_id, earn_data }, GemTransactionLoadMetadata::Near { sequence, block_hash } => TransactionLoadMetadata::Near { sequence, block_hash }, GemTransactionLoadMetadata::Stellar { sequence, @@ -680,6 +706,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, earn_type) => GemTransactionInputType::Earn { asset, earn_type }, } } } @@ -831,6 +858,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, earn_type } => TransactionInputType::Earn(asset, earn_type), } } } 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 }