diff --git a/backend/.env.exemple b/backend/.env.example similarity index 54% rename from backend/.env.exemple rename to backend/.env.example index eec7f51..0067082 100644 --- a/backend/.env.exemple +++ b/backend/.env.example @@ -1,3 +1,5 @@ CONTRACT_ADDRESS="0xbA276291a3EFE899b5B5fB2DFFd513B7347E11D7" PRIVATE_KEY="your_private_key_here" +COINGECKO_API_KEY="your_coingecko_api_key_here" +GEMINI_API_KEY="your_gemini_api_key_here" PORT=8080 \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 7089e43..f7d01fc 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1206,6 +1206,12 @@ dependencies = [ "serde", ] +[[package]] +name = "as-any" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" + [[package]] name = "async-stream" version = "0.3.6" @@ -1915,6 +1921,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -2101,6 +2118,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -2857,6 +2880,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2896,6 +2925,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3035,6 +3074,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3506,6 +3554,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", @@ -3519,12 +3568,14 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 1.0.4", ] @@ -3539,6 +3590,35 @@ dependencies = [ "subtle", ] +[[package]] +name = "rig-core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112bd2e51d1fadd16509be2b3d45bfa197725787627f6addcaffe57294f64e18" +dependencies = [ + "as-any", + "async-stream", + "base64", + "bytes", + "eventsource-stream", + "futures", + "futures-timer", + "glob", + "http 1.3.1", + "mime_guess", + "ordered-float", + "pin-project-lite", + "reqwest", + "schemars 1.1.0", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-futures", + "url", +] + [[package]] name = "ring" version = "0.17.14" @@ -3771,10 +3851,23 @@ checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", + "schemars_derive", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.110", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3900,6 +3993,17 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "serde_json" version = "1.0.145" @@ -4594,6 +4698,18 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "futures", + "futures-task", + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -4899,6 +5015,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -5278,6 +5407,7 @@ dependencies = [ "futures", "once_cell", "reqwest", + "rig-core", "serde", "serde_json", "tokio", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7d97a27..afece6c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -13,6 +13,7 @@ dotenvy = "0.15.7" futures = "0.3.31" once_cell = "1.21.3" reqwest = "0.12.24" +rig-core = "0.24.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" tokio = { version = "1.48.0", features = ["sync"] } diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 8a9cf62..a9c91c4 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -1,6 +1,11 @@ use actix_web::{HttpResponse, Responder, get, post, web}; -use crate::{state::AppState, types::Pool}; +use crate::{ + core::{self, coingecko::CoingeckoOhlcvRes}, + state::AppState, + strategies, + types::{Pool, RangeSuggestion}, +}; #[utoipa::path( responses( @@ -36,3 +41,41 @@ async fn get_pools_service(app_state: web::Data) -> impl Responder { .collect(); HttpResponse::Ok().json(pools) } + +#[utoipa::path( + responses( + (status = 200, description = "Pool", body = CoingeckoOhlcvRes), + ) +)] +#[get("/pool/{pool_address}/coingecko/ohlcv")] +async fn get_pool_coingecko_ohlcv_service(pool_address: web::Path) -> impl Responder { + let pool_address = pool_address.into_inner(); + + let ohlcv_data_result = match core::coingecko::get_pool_ohlcv_data(&pool_address).await { + Ok(data) => data, + Err(err) => { + return HttpResponse::InternalServerError() + .body(format!("Error fetching OHLCV data: {}", err)); + } + }; + + HttpResponse::Ok().json(ohlcv_data_result) +} + +#[utoipa::path( + responses( + (status = 200, description = "Suggested liquidity range", body = RangeSuggestion), + ) +)] +#[get("/pools/{pool_address}/liquidity/suggest")] +async fn suggest_liquidity_range_service( + app_state: web::Data, + path: web::Path, +) -> impl Responder { + let pool_address = path.into_inner(); + + match strategies::default::suggest_liquidity_range(&app_state, &pool_address).await { + Ok(suggestion) => HttpResponse::Ok().json(suggestion), + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + } +} diff --git a/backend/src/config/bnb.toml b/backend/src/config/bnb.toml index e743aad..585eca2 100644 --- a/backend/src/config/bnb.toml +++ b/backend/src/config/bnb.toml @@ -1,6 +1,7 @@ [chain] rpc_url = "https://bsc-dataseed.binance.org/" chain_id = 56 +coingecko_id = "bsc" [[pools]] address = "0xaeaD6bd31dd66Eb3A6216aAF271D0E661585b0b1" diff --git a/backend/src/config/mod.rs b/backend/src/config/mod.rs index d0d2635..64c2fb2 100644 --- a/backend/src/config/mod.rs +++ b/backend/src/config/mod.rs @@ -15,6 +15,7 @@ pub struct TomlConfig { pub struct ChainConfig { pub rpc_url: String, pub chain_id: u64, + pub coingecko_id: String, } #[derive(Debug, Deserialize, Clone)] @@ -28,6 +29,7 @@ pub struct PoolConfig { pub struct Config { pub contract_address: String, pub private_key: String, + pub coingecko_api_key: String, pub port: u16, pub toml: TomlConfig, } @@ -37,6 +39,8 @@ impl Config { let contract_address = std::env::var("CONTRACT_ADDRESS").expect("CONTRACT_ADDRESS must be set"); let private_key = std::env::var("PRIVATE_KEY").expect("PRIVATE_KEY must be set"); + let coingecko_api_key = + std::env::var("COINGECKO_API_KEY").expect("COINGECKO_API_KEY must be set"); let port: u16 = std::env::var("PORT") .unwrap_or_else(|_| "8080".to_string()) .parse() @@ -52,6 +56,7 @@ impl Config { Self { contract_address, private_key, + coingecko_api_key, port, toml: config, } @@ -66,4 +71,4 @@ pub const FEE_FACTOR: f64 = 10_000.0; /// Maximum number of concurrent tasks allowed /// This prevents overwhelming -pub const MAX_ALLOWED_THREADS: usize = 8; \ No newline at end of file +pub const MAX_ALLOWED_THREADS: usize = 8; diff --git a/backend/src/core/coingecko.rs b/backend/src/core/coingecko.rs new file mode 100644 index 0000000..b9926e7 --- /dev/null +++ b/backend/src/core/coingecko.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::info; +use utoipa::ToSchema; + +use crate::config::CONFIG; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct CoingeckoOhlcvRes { + pub data: CoingeckoResData, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct CoingeckoResData { + pub id: String, + pub attributes: CoingeckoResDataAttributes, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct CoingeckoResDataAttributes { + pub ohlcv_list: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct OhlcvEntry( + i64, // timestamp (UNIX) + f64, // open + f64, // high + f64, // low + f64, // close + f64, // volume +); + +pub async fn get_pool_ohlcv_data(pool_address: &str) -> Result { + let url = format!( + "https://api.coingecko.com/api/v3/onchain/networks/{}/pools/{}/ohlcv/day?token=base¤cy=token&limit=1000", + CONFIG.toml.chain.coingecko_id, pool_address + ); + + let coingecko_api_key = &CONFIG.coingecko_api_key; + + // Set up headers + let mut headers = HeaderMap::new(); + + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + headers.insert( + "x-cg-demo-api-key", + HeaderValue::from_str(&coingecko_api_key)?, + ); + + // Make request + let client = reqwest::Client::new(); + + let response = client.get(url).headers(headers).send().await?; + + let ohlcv_data_res: Value = response.json().await?; + + let ohlcv_data: CoingeckoOhlcvRes = serde_json::from_value(ohlcv_data_res)?; + + info!( + "Coingecko data fetched successfully: {:?}", + ohlcv_data.data.attributes.ohlcv_list.len() + ); + + Ok(ohlcv_data) +} diff --git a/backend/src/core/init.rs b/backend/src/core/init.rs index f8f345f..0cfc5a0 100644 --- a/backend/src/core/init.rs +++ b/backend/src/core/init.rs @@ -6,6 +6,7 @@ use alloy::{providers::ProviderBuilder, signers::local::PrivateKeySigner}; use anyhow::Result; use dashmap::DashMap; use futures::stream::{self, StreamExt, TryStreamExt}; +use rig::{agent::Agent, client::CompletionClient, providers::gemini::{self, completion::{CompletionModel, gemini_api_types::{AdditionalParameters, GenerationConfig, ThinkingConfig}}}}; use tokio::sync::Semaphore; use tracing::{debug, info}; @@ -198,3 +199,32 @@ pub async fn init_pools_state(evm_provider: &EvmProvider) -> Result - there are still active references") }) } + + +/// Initialize the AI agent using the Google Gemini provider +pub async fn init_ai_agent() -> Result> { + // Initialize the Google Gemini client + let client = gemini::Client::from_env(); + + let gen_cfg = GenerationConfig { + thinking_config: Some(ThinkingConfig { + thinking_budget: 10, + include_thoughts: None, + }), + ..Default::default() + }; + + let cfg = AdditionalParameters::default().with_config(gen_cfg); + + // Create agent with a single context prompt + let agent = client + .agent("gemini-flash-latest") + .preamble("You are a liquidity manager AI assistant. Your goal is to help users optimize their Liquidity provision strategies on uniswap V3 pools on EVM-compatible blockchains by suggesting the best price range to provide liquidity based on current market conditions and historical data (data will be provided to you on the prompt by coingecko). Always Respect teh given JSON schema in your answers. and only return JSON objects only.") + .temperature(0.0) + .additional_params(serde_json::to_value(cfg)?) + .build(); + + tracing::info!("AI Agent initialized successfully."); + + Ok(agent) +} diff --git a/backend/src/core/mod.rs b/backend/src/core/mod.rs index c60ce45..fc93b6d 100644 --- a/backend/src/core/mod.rs +++ b/backend/src/core/mod.rs @@ -1,2 +1,3 @@ -pub mod pools; +pub mod coingecko; pub mod init; +pub mod pools; diff --git a/backend/src/main.rs b/backend/src/main.rs index 99c2207..2f2f2be 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,6 +11,7 @@ mod api; mod config; mod core; mod state; +mod strategies; mod types; mod utils; @@ -64,6 +65,8 @@ async fn main() -> std::io::Result<()> { .service(api::get_index_service) .service(api::get_health_service) .service(api::get_pools_service) + .service(api::get_pool_coingecko_ohlcv_service) + .service(api::suggest_liquidity_range_service) .split_for_parts(); app.service(SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", app_api)) diff --git a/backend/src/state.rs b/backend/src/state.rs index 31fa290..861761b 100644 --- a/backend/src/state.rs +++ b/backend/src/state.rs @@ -1,19 +1,26 @@ use dashmap::DashMap; +use rig::{agent::Agent, providers::gemini::completion::CompletionModel}; use tracing::info; use crate::{ - core, + core::{self, init::init_ai_agent}, types::{EvmProvider, Pool}, }; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct AppState { pub evm_provider: EvmProvider, pub pools: DashMap, + pub ai_agent: Agent, } impl AppState { pub async fn new() -> Self { + // Initialize the AI agent + let ai_agent = init_ai_agent() + .await + .expect("Failed to initialize AI agent"); + let evm_provider = core::init::init_evm_provider() .await .expect("Failed to initialize EVM provider"); @@ -26,6 +33,7 @@ impl AppState { Self { evm_provider, pools, + ai_agent, } } } diff --git a/backend/src/strategies/default.rs b/backend/src/strategies/default.rs new file mode 100644 index 0000000..b58b299 --- /dev/null +++ b/backend/src/strategies/default.rs @@ -0,0 +1,55 @@ +use actix_web::web; +use anyhow::Result; +use rig::completion::Prompt; +use serde::{Deserialize, Serialize}; + +use crate::{ + core::coingecko, + state::AppState, + types::{RangeSuggestion, StrategyType}, + utils::extract_json_from_markdown, +}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AIResponse { + pub low_price: f64, + pub upper_price: f64, + pub confidence: f64, + pub reason: String, +} + +pub async fn suggest_liquidity_range( + app_state: &web::Data, + pool_address: &str, +) -> Result { + let ai_agent = &app_state.ai_agent; + + // Fetch coingecko data for the pool + let coingecko_ohlcv_data = coingecko::get_pool_ohlcv_data(pool_address).await?; + + // Use the AI agent to analyze data and suggest a price range + let analysis_prompt = format!( + "Analyze the following OHLCV data and suggest a liquidity range for this pool '{}' based on its history and current market conditions.Identify the general trend, volatility, and any significant events that may have impacted the pool's price. I want teh raneg to be the optimal one to earn more yields in less time but if market are not good use maybe a wider oen for safety. Return the lower price and the upper price, the reason for the suggestion and the confidence level. The reason field should be very explainatory about the decision you've maked. Return only Json object that respect this Schema: {{\"low_price\": number, \"upper_price\": number, \"confidence\": number, \"reason\": string}}. Tis is the coingecko pool history data: {:?}", + pool_address, coingecko_ohlcv_data + ); + + tracing::debug!("Prompt sent to AI agent..."); + + let ai_response_str = ai_agent.prompt(analysis_prompt).await?; + + tracing::info!("AI Response: {:?}", ai_response_str); + + let formatted_md_response = extract_json_from_markdown(&ai_response_str); + + let ai_response: AIResponse = serde_json::from_str(&formatted_md_response)?; + + tracing::info!("AI Response: {:?}", ai_response); + + Ok(RangeSuggestion { + up_price: ai_response.upper_price, + down_price: ai_response.low_price, + confidence: ai_response.confidence, + reason: ai_response.reason, + strategy: StrategyType::Default, + }) +} diff --git a/backend/src/strategies/mod.rs b/backend/src/strategies/mod.rs new file mode 100644 index 0000000..1be8d34 --- /dev/null +++ b/backend/src/strategies/mod.rs @@ -0,0 +1 @@ +pub mod default; diff --git a/backend/src/types.rs b/backend/src/types.rs index 8eeef86..2f8d906 100644 --- a/backend/src/types.rs +++ b/backend/src/types.rs @@ -60,3 +60,17 @@ pub struct Token { pub symbol: String, pub decimals: u8, } + +#[derive(Debug, Deserialize, Clone, Serialize, ToSchema)] +pub enum StrategyType { + Default, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct RangeSuggestion { + pub up_price: f64, + pub down_price: f64, + pub confidence: f64, + pub reason: String, + pub strategy: StrategyType, +} diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs index 861e229..d316af7 100644 --- a/backend/src/utils/mod.rs +++ b/backend/src/utils/mod.rs @@ -1 +1,6 @@ pub mod amm_math; + +pub fn extract_json_from_markdown(md: &str) -> String { + let json_block = md.replace("```json", "").replace("```", ""); + json_block.trim().to_string() +}