From 9ca339b870315b8c23fa4e26414e2774579f4884 Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Wed, 29 Jan 2025 05:57:24 +0900 Subject: [PATCH 1/8] feat: init --- Cargo.lock | 199 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 ++ api/Cargo.toml | 32 +++++++ api/README.md | 1 + api/src/bin/main.rs | 1 + api/src/lib.rs | 14 ++++ 6 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 api/Cargo.toml create mode 100644 api/README.md create mode 100644 api/src/bin/main.rs create mode 100644 api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 338996ba..8a7eee59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,55 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -2129,6 +2178,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -2382,6 +2437,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "jito-restaking-api" +version = "0.0.3" +dependencies = [ + "anchor-lang", + "axum", + "clap 4.5.16", + "http", + "serde", + "serde_json", + "solana-program", + "solana-rpc-client", + "solana-rpc-client-api", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "jito-restaking-cli" version = "0.0.3" @@ -2801,6 +2878,21 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.1" @@ -2956,6 +3048,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.2.1" @@ -3213,6 +3315,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -3677,8 +3785,17 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3689,9 +3806,15 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.3", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.3" @@ -4022,6 +4145,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.7" @@ -6386,6 +6519,48 @@ dependencies = [ "winnow 0.6.18", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util 0.7.10", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.2" @@ -6425,6 +6600,17 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-opentelemetry" version = "0.17.4" @@ -6444,9 +6630,16 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", + "smallvec", "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf2ac2b6..498cfcd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "account_traits_derive", + "api", "bytemuck", "cli", "clients/rust/restaking_client", @@ -41,6 +42,7 @@ readme = "README.md" anchor-lang = { version = "0.30.1", features = ["idl-build"] } anyhow = "1.0.86" assert_matches = "1.5.0" +axum = "0.6.2" borsh = { version = "0.10.3" } bytemuck = { version = "1.16.3", features = ["min_const_generics"] } cfg-if = "1.0.0" @@ -52,6 +54,7 @@ dotenv = "0.15.0" envfile = "0.2.1" env_logger = "0.10.2" futures = "0.3.31" +http = "0.2.1" jito-bytemuck = { path = "bytemuck", version = "=0.0.3" } jito-account-traits-derive = { path = "account_traits_derive", version = "=0.0.3" } jito-jsm-core = { path = "core", version = "=0.0.3" } @@ -70,6 +73,7 @@ num-traits = "0.2.19" proc-macro2 = "1.0.86" quote = "1.0.36" serde = { version = "^1.0", features = ["derive"] } +serde_json = "1.0.102" serde_with = "3.9.0" shank = "0.4.2" shank_idl = "0.4.2" @@ -89,3 +93,8 @@ syn = "2.0.72" test-case = "3.3.1" thiserror = "1.0.57" tokio = { version = "1.36.0", features = ["full"] } +tower = { version = "0.4.13", features = ["limit", "buffer", "timeout", "load-shed"] } +tower-http = { version = "0.4.0", features = ["trace"] } +tracing = { version = "0.1.37" } +tracing-core = "0.1.32" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 00000000..51bb1d99 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "jito-restaking-api" +description = "Jito Restaking API" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +readme = { workspace = true } + +[[bin]] +name = "jito-restaking-api" +path = "src/bin/main.rs" + +[dependencies] +anchor-lang = { workspace = true } +axum = { workspace = true } +clap = { workspace = true } +http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +solana-program = { workspace = true } +solana-rpc-client = { workspace = true } +solana-rpc-client-api = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } +tracing-core = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..3f4c27e5 --- /dev/null +++ b/api/README.md @@ -0,0 +1 @@ +# Jito Restaking API diff --git a/api/src/bin/main.rs b/api/src/bin/main.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/api/src/bin/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 7d369a2fa5455fb95f6b8b24ea87f84c54cd5339 Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Wed, 29 Jan 2025 07:11:27 +0900 Subject: [PATCH 2/8] feat: list vaults and get vault by pubkey --- Cargo.lock | 4 + api/Cargo.toml | 4 + api/README.md | 215 ++++++++++++++++++++++++++++ api/src/bin/main.rs | 43 +++++- api/src/error.rs | 91 ++++++++++++ api/src/lib.rs | 124 ++++++++++++++-- api/src/router/mod.rs | 72 ++++++++++ api/src/router/vault/get_vault.rs | 36 +++++ api/src/router/vault/list_vaults.rs | 57 ++++++++ api/src/router/vault/mod.rs | 2 + 10 files changed, 633 insertions(+), 15 deletions(-) create mode 100644 api/src/error.rs create mode 100644 api/src/router/mod.rs create mode 100644 api/src/router/vault/get_vault.rs create mode 100644 api/src/router/vault/list_vaults.rs create mode 100644 api/src/router/vault/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8a7eee59..bef8e02f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,8 +2445,12 @@ dependencies = [ "axum", "clap 4.5.16", "http", + "jito-bytemuck", + "jito-vault-client", + "jito-vault-core", "serde", "serde_json", + "solana-account-decoder", "solana-program", "solana-rpc-client", "solana-rpc-client-api", diff --git a/api/Cargo.toml b/api/Cargo.toml index 51bb1d99..c79c78bc 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -18,8 +18,12 @@ anchor-lang = { workspace = true } axum = { workspace = true } clap = { workspace = true } http = { workspace = true } +jito-bytemuck = { workspace = true } +jito-vault-client = { workspace = true, features = ["serde"] } +jito-vault-core = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +solana-account-decoder = { workspace = true } solana-program = { workspace = true } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } diff --git a/api/README.md b/api/README.md index 3f4c27e5..66c786a1 100644 --- a/api/README.md +++ b/api/README.md @@ -1 +1,216 @@ # Jito Restaking API + +# StakeNet API + +## Overview + +The StakeNet API provides access to historical validator performance data on the Solana blockchain. This API can be useful for any website or application that needs to show validator performance history, specific epoch information, or the latest validator data. + +## Getting started + +### Prerequisites + +- [Solana's RPC Client](https://docs.rs/solana-rpc-client/latest/solana_rpc_client/) +- [Axum](https://docs.rs/axum/latest/axum/) + +### Build for release + +To build the API for release, run the following command: + +```bash +cargo b --release --bin jito-restaking-api +``` + +### Check available options + +To view the options available for configuring the API: + +```bash +./target/release/jito-restaking-api --help + +# Usage: jito-stakenet-api [OPTIONS] +# +# Options: +# --bind-addr +# Bind address for the server [env: BIND_ADDR=] [default: 0.0.0.0:7001] +# --rpc-url +# RPC url [env: JSON_RPC_URL=] [default: https://api.mainnet-beta.solana.com] +# -h, --help +# Print help +# -V, --version +# Print version +``` + +### Running the API + +Once built, run the API using the following command: + +```bash +./target/release/jito-stakenet-api +``` + +You can now send requests to http://localhost:7001 (or whichever address/port you specify in --bind-addr). + +## API Endpoints + +|HTTP Method|Endpoint |Description | +|-----------|---------------------------------|----------------------| +|GET |/api/v1/vault/list |Fetch all vaults | + + +### Example Requests + +#### Get all vaults: + +``` +curl http://localhost:7001/api/v1/vault/list +``` + + +## Tips for Developers + +### Add a New Route + +If you want to add a new route to the router, you do it in `api/src/router.rs`: + +```rust +// api/src/router.rs + +let validator_history_routes = Router::new() + .route( + "/:vote_account", + get(get_all_validator_histories::get_all_validator_histories), + ) + .route( + "/:vote_account/latest", + get(get_latest_validator_history::get_latest_validator_history), + ) + .route( + "/:vote_account/new_route", + get(new_method), + ); +``` + +### Caching Validator History + +You can implement a caching layer for the validator histories using the [Moka](https://docs.rs/moka/latest/moka/index.html) library. Here's an example of adding caching to the server. + +#### Step 1: Add Moka Dependency + +```bash +cargo add moka --features future +``` + +#### Step 2: Update State to Include Cache + +```rust +// api/src/router.rs + +pub struct RouterState { + pub validator_history_program_id: Pubkey, + pub rpc_client: RpcClient, + + // add cache + pub cache: Cache, +} +``` + +#### Step 3: Modify Main To Use Cache + +```rust +// api/src/bin/main.rs + +#[tokio::main] +#[instrument] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + tracing_subscriber::fmt().init(); + + info!("args: {:?}", args); + + info!("starting server at {}", args.bind_addr); + + let rpc_client = RpcClient::new(args.json_rpc_url.clone()); + info!("started rpc client at {}", args.json_rpc_url); + + // Create a cache that can store up to u64::MAX entries. + let cache: Cache = Cache::new(u64::MAX); + + let state = Arc::new(jito_stakenet_api::router::RouterState { + validator_history_program_id: args.validator_history_program_id, + rpc_client, + cache, + }); + + let app = jito_stakenet_api::router::get_routes(state); + + axum::Server::bind(&args.bind_addr) + .serve(app.into_make_service_with_connect_info::()) + .await?; + + Ok(()) +} +``` + +#### Step 4: Use Cache in Handlers + +```rust +// api/src/router/get_all_validator_histories.rs + +pub(crate) async fn get_all_validator_histories( + State(state): State>, + Path(vote_account): Path, + Query(epoch_query): Query, +) -> crate::Result { + let vote_account = Pubkey::from_str(&vote_account)?; + let history_account = + get_validator_history_address(&vote_account, &state.validator_history_program_id); + + // Check history_account's pubkey key in cache + match state.cache.get(&history_account).await { + Some(history) => Ok(Json(history)), + None => { + let account = state.rpc_client.get_account(&history_account).await?; + let validator_history = ValidatorHistory::try_deserialize(&mut account.data.as_slice()) + .map_err(|e| { + warn!("error deserializing ValidatorHistory: {:?}", e); + ApiError::ValidatorHistoryError("Error parsing ValidatorHistory".to_string()) + })?; + + let history_entries: Vec = match epoch_query.epoch { + Some(epoch) => validator_history + .history + .arr + .iter() + .filter_map(|entry| { + if epoch == entry.epoch { + Some(ValidatorHistoryEntryResponse::from_validator_history_entry( + entry, + )) + } else { + None + } + }) + .collect(), + None => validator_history + .history + .arr + .iter() + .map(ValidatorHistoryEntryResponse::from_validator_history_entry) + .collect(), + }; + + let history = ValidatorHistoryResponse::from_validator_history( + validator_history, + history_entries, + ); + + // Insert new history in cache + state.cache.insert(history_account, history.clone()).await; + + Ok(Json(history)) + } + } +} +``` diff --git a/api/src/bin/main.rs b/api/src/bin/main.rs index f328e4d9..f50c7466 100644 --- a/api/src/bin/main.rs +++ b/api/src/bin/main.rs @@ -1 +1,42 @@ -fn main() {} +use std::{net::SocketAddr, str::FromStr, sync::Arc}; + +use clap::Parser; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; +use tracing::{info, instrument}; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + /// Bind address for the server + #[arg(long, env, default_value_t = SocketAddr::from_str("0.0.0.0:7001").unwrap())] + pub bind_addr: SocketAddr, + + /// RPC url + #[arg(long, env, default_value = "https://api.mainnet-beta.solana.com")] + pub rpc_url: String, +} + +#[tokio::main] +#[instrument] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + tracing_subscriber::fmt().init(); + + info!("args: {:?}", args); + + info!("starting server at {}", args.bind_addr); + + let rpc_client = RpcClient::new(args.rpc_url.clone()); + info!("started rpc client at {}", args.rpc_url); + + let state = Arc::new(jito_restaking_api::router::RouterState { rpc_client }); + + let app = jito_restaking_api::router::get_routes(state); + + axum::Server::bind(&args.bind_addr) + .serve(app.into_make_service_with_connect_info::()) + .await?; + + Ok(()) +} diff --git a/api/src/error.rs b/api/src/error.rs new file mode 100644 index 00000000..9d1fa010 --- /dev/null +++ b/api/src/error.rs @@ -0,0 +1,91 @@ +use std::convert::Infallible; + +use axum::{ + response::{IntoResponse, Response}, + BoxError, Json, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use solana_program::pubkey::ParsePubkeyError; +use solana_rpc_client_api::client_error::Error as RpcError; +use thiserror::Error; +use tracing::error; + +#[derive(Error, Debug)] +pub enum JitoRestakingApiError { + #[error("Rpc Error")] + RpcError(#[from] RpcError), + + #[error("Parse Pubkey Error")] + ParsePubkeyError(#[from] ParsePubkeyError), + + #[error("Anchor Error")] + AnchorError(#[from] anchor_lang::error::Error), + + #[error("Internal Error")] + InternalError, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Error { + pub error: String, +} + +impl IntoResponse for JitoRestakingApiError { + fn into_response(self) -> Response { + let (status, error_message) = match self { + JitoRestakingApiError::RpcError(e) => { + error!("Rpc error: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, "Rpc error") + } + JitoRestakingApiError::ParsePubkeyError(e) => { + error!("Parse pubkey error: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, "Pubkey parse error") + } + JitoRestakingApiError::AnchorError(e) => { + error!("Anchor error: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") + } + JitoRestakingApiError::InternalError => { + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") + } + }; + ( + status, + Json(Error { + error: error_message.to_string(), + }), + ) + .into_response() + } +} + +pub async fn handle_error(error: BoxError) -> Result { + if error.is::() { + return Ok(( + StatusCode::REQUEST_TIMEOUT, + Json(json!({ + "code" : 408, + "error" : "Request Timeout", + })), + )); + }; + if error.is::() { + return Ok(( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ + "code" : 503, + "error" : "Service Unavailable", + })), + )); + } + + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "code" : 500, + "error" : "Internal Server Error", + })), + )) +} diff --git a/api/src/lib.rs b/api/src/lib.rs index b93cf3ff..6507968d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,14 +1,110 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +use error::JitoRestakingApiError; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; + +pub mod error; +pub mod router; + +pub type Result = std::result::Result; + +// #[derive(Serialize, Deserialize)] +// pub(crate) struct ValidatorHistoryResponse { +// /// Cannot be enum due to Pod and Zeroable trait limitations +// pub(crate) struct_version: u32, +// +// pub(crate) vote_account: Pubkey, +// /// Index of validator of all ValidatorHistory accounts +// pub(crate) index: u32, +// +// /// These Crds gossip values are only signed and dated once upon startup and then never updated +// /// so we track latest time on-chain to make sure old messages aren't uploaded +// pub(crate) last_ip_timestamp: u64, +// pub(crate) last_version_timestamp: u64, +// +// pub(crate) history: Vec, +// } +// +// impl ValidatorHistoryResponse { +// pub fn from_validator_history( +// acc: ValidatorHistory, +// history_entries: Vec, +// ) -> Self { +// Self { +// struct_version: acc.struct_version, +// vote_account: acc.vote_account, +// index: acc.index, +// last_ip_timestamp: acc.last_ip_timestamp, +// last_version_timestamp: acc.last_version_timestamp, +// history: history_entries, +// } +// } +// } +// +// #[derive(Serialize, Deserialize)] +// pub(crate) struct ValidatorHistoryEntryResponse { +// pub(crate) activated_stake_lamports: u64, +// pub(crate) epoch: u16, +// +// // MEV commission in basis points +// pub(crate) mev_commission: u16, +// +// // Number of successful votes in current epoch. Not finalized until subsequent epoch +// pub(crate) epoch_credits: u32, +// +// // Validator commission in points +// pub(crate) commission: u8, +// +// // 0 if Solana Labs client, 1 if Jito client, >1 if other +// pub(crate) client_type: u8, +// pub(crate) version: ClientVersionResponse, +// pub(crate) ip: [u8; 4], +// +// // 0 if not a superminority validator, 1 if superminority validator +// pub(crate) is_superminority: u8, +// +// // rank of validator by stake amount +// pub(crate) rank: u32, +// +// // Most recent updated slot for epoch credits and commission +// pub(crate) vote_account_last_update_slot: u64, +// +// // MEV earned, stored as 1/100th SOL. mev_earned = 100 means 1.00 SOL earned +// pub(crate) mev_earned: u32, +// } + +// impl ValidatorHistoryEntryResponse { +// pub fn from_validator_history_entry(entry: &ValidatorHistoryEntry) -> Self { +// let version = ClientVersionResponse::from_client_version(entry.version); +// Self { +// activated_stake_lamports: entry.activated_stake_lamports, +// epoch: entry.epoch, +// mev_commission: entry.mev_commission, +// epoch_credits: entry.epoch_credits, +// commission: entry.commission, +// client_type: entry.client_type, +// version, +// ip: entry.ip, +// is_superminority: entry.is_superminority, +// rank: entry.rank, +// vote_account_last_update_slot: entry.vote_account_last_update_slot, +// mev_earned: entry.mev_earned, +// } +// } +// } +// +// #[derive(Serialize, Deserialize)] +// pub(crate) struct ClientVersionResponse { +// pub(crate) major: u8, +// pub(crate) minor: u8, +// pub(crate) patch: u16, +// } +// +// impl ClientVersionResponse { +// pub fn from_client_version(version: ClientVersion) -> Self { +// Self { +// major: version.major, +// minor: version.minor, +// patch: version.patch, +// } +// } +// } diff --git a/api/src/router/mod.rs b/api/src/router/mod.rs new file mode 100644 index 00000000..a9892e2b --- /dev/null +++ b/api/src/router/mod.rs @@ -0,0 +1,72 @@ +mod vault; + +use std::{sync::Arc, time::Duration}; + +use axum::{ + body::Body, error_handling::HandleErrorLayer, response::IntoResponse, routing::get, Router, +}; +use http::StatusCode; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; +use tower::{ + buffer::BufferLayer, limit::RateLimitLayer, load_shed::LoadShedLayer, timeout::TimeoutLayer, + ServiceBuilder, +}; +use tower_http::{ + trace::{DefaultOnResponse, TraceLayer}, + LatencyUnit, +}; +use tracing::{info, instrument, Span}; +use vault::{get_vault::get_vault, list_vaults::list_vaults}; + +pub struct RouterState { + pub rpc_client: RpcClient, +} + +impl std::fmt::Debug for RouterState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RouterState") + .field("rpc_client", &self.rpc_client.url()) + .finish() + } +} + +#[instrument] +pub fn get_routes(state: Arc) -> Router { + let middleware = ServiceBuilder::new() + .layer(HandleErrorLayer::new(crate::error::handle_error)) + .layer(BufferLayer::new(1000)) + .layer(RateLimitLayer::new(10000, Duration::from_secs(1))) + .layer(TimeoutLayer::new(Duration::from_secs(20))) + .layer(LoadShedLayer::new()) + .layer( + TraceLayer::new_for_http() + .on_request(|request: &http::Request, _span: &Span| { + info!("started {} {}", request.method(), request.uri().path()) + }) + .on_response( + DefaultOnResponse::new() + .level(tracing_core::Level::INFO) + .latency_unit(LatencyUnit::Millis), + ), + ); + + let vault_routes = Router::new() + .route("/list", get(list_vaults)) + .route("/:vault_pubkey", get(get_vault)); + + let api_routes = Router::new() + .route("/", get(root)) + .nest("/vault", vault_routes); + + let app = Router::new().nest("/api/v1", api_routes).fallback(fallback); + + app.layer(middleware).with_state(state) +} + +async fn root() -> impl IntoResponse { + "Jito Restaking API" +} + +async fn fallback() -> (StatusCode, &'static str) { + (StatusCode::NOT_FOUND, "Not Found") +} diff --git a/api/src/router/vault/get_vault.rs b/api/src/router/vault/get_vault.rs new file mode 100644 index 00000000..3dfc6336 --- /dev/null +++ b/api/src/router/vault/get_vault.rs @@ -0,0 +1,36 @@ +use std::{str::FromStr, sync::Arc}; + +use anchor_lang::{prelude::Pubkey, AnchorDeserialize}; +use axum::{ + extract::{Path, State}, + response::IntoResponse, + Json, +}; +use jito_vault_client::accounts::Vault; + +use crate::{error::JitoRestakingApiError, router::RouterState}; + +/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. +/// +/// # Returns +/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. +/// +/// # Example +/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: +/// ``` +/// GET /validator_history/{vote_account}?epoch=200 +/// ``` +/// This request retrieves the history for the specified vote account, filtered by epoch 200. +pub(crate) async fn get_vault( + State(state): State>, + Path(vault_pubkey): Path, +) -> crate::Result { + let vault_pubkey = Pubkey::from_str(&vault_pubkey)?; + let account = state.rpc_client.get_account(&vault_pubkey).await?; + let vault = Vault::deserialize(&mut account.data.as_slice()).map_err(|e| { + tracing::warn!("error deserializing Vault: {:?}", e); + JitoRestakingApiError::AnchorError(e.into()) + })?; + + Ok(Json(vault)) +} diff --git a/api/src/router/vault/list_vaults.rs b/api/src/router/vault/list_vaults.rs new file mode 100644 index 00000000..8b0ad27f --- /dev/null +++ b/api/src/router/vault/list_vaults.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use anchor_lang::AnchorDeserialize; +use axum::{extract::State, response::IntoResponse, Json}; +use jito_bytemuck::Discriminator; +use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; +use solana_account_decoder::UiAccountEncoding; +use solana_rpc_client_api::{ + config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; + +use crate::router::RouterState; + + +/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. +/// +/// # Returns +/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. +/// +/// # Example +/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: +/// ``` +/// GET /validator_history/{vote_account}?epoch=200 +/// ``` +/// This request retrieves the history for the specified vote account, filtered by epoch 200. +pub(crate) async fn list_vaults( + State(state): State>, +) -> crate::Result { + let accounts = state + .rpc_client + .get_program_accounts_with_config( + &JITO_VAULT_ID, + RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new( + 0, + MemcmpEncodedBytes::Bytes(vec![jito_vault_core::vault::Vault::DISCRIMINATOR]), + ))]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + }, + ) + .await?; + + let mut vaults = Vec::new(); + for (_vault_pubkey, vault) in accounts { + let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); + vaults.push(vault); + } + + Ok(Json(vaults)) +} diff --git a/api/src/router/vault/mod.rs b/api/src/router/vault/mod.rs new file mode 100644 index 00000000..24183a93 --- /dev/null +++ b/api/src/router/vault/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod get_vault; +pub(crate) mod list_vaults; From 14cae762d1d2ce73f5d2bcd6b8f38331876006ca Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Wed, 29 Jan 2025 08:12:59 +0900 Subject: [PATCH 3/8] feat: get tvl --- api/src/router/mod.rs | 7 +- api/src/router/{vault => vaults}/get_vault.rs | 0 .../router/{vault => vaults}/list_vaults.rs | 0 api/src/router/{vault => vaults}/mod.rs | 1 + api/src/router/vaults/tvl.rs | 75 +++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) rename api/src/router/{vault => vaults}/get_vault.rs (100%) rename api/src/router/{vault => vaults}/list_vaults.rs (100%) rename api/src/router/{vault => vaults}/mod.rs (72%) create mode 100644 api/src/router/vaults/tvl.rs diff --git a/api/src/router/mod.rs b/api/src/router/mod.rs index a9892e2b..22a541a2 100644 --- a/api/src/router/mod.rs +++ b/api/src/router/mod.rs @@ -1,4 +1,4 @@ -mod vault; +mod vaults; use std::{sync::Arc, time::Duration}; @@ -16,7 +16,7 @@ use tower_http::{ LatencyUnit, }; use tracing::{info, instrument, Span}; -use vault::{get_vault::get_vault, list_vaults::list_vaults}; +use vaults::{get_vault::get_vault, list_vaults::list_vaults, tvl::get_tvls}; pub struct RouterState { pub rpc_client: RpcClient, @@ -52,7 +52,8 @@ pub fn get_routes(state: Arc) -> Router { let vault_routes = Router::new() .route("/list", get(list_vaults)) - .route("/:vault_pubkey", get(get_vault)); + .route("/:vault_pubkey", get(get_vault)) + .route("/tvls", get(get_tvls)); let api_routes = Router::new() .route("/", get(root)) diff --git a/api/src/router/vault/get_vault.rs b/api/src/router/vaults/get_vault.rs similarity index 100% rename from api/src/router/vault/get_vault.rs rename to api/src/router/vaults/get_vault.rs diff --git a/api/src/router/vault/list_vaults.rs b/api/src/router/vaults/list_vaults.rs similarity index 100% rename from api/src/router/vault/list_vaults.rs rename to api/src/router/vaults/list_vaults.rs diff --git a/api/src/router/vault/mod.rs b/api/src/router/vaults/mod.rs similarity index 72% rename from api/src/router/vault/mod.rs rename to api/src/router/vaults/mod.rs index 24183a93..eeccdfdf 100644 --- a/api/src/router/vault/mod.rs +++ b/api/src/router/vaults/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod get_vault; pub(crate) mod list_vaults; +pub(crate) mod tvl; diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs new file mode 100644 index 00000000..c8b62435 --- /dev/null +++ b/api/src/router/vaults/tvl.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use anchor_lang::AnchorDeserialize; +use axum::{extract::State, response::IntoResponse, Json}; +use jito_bytemuck::Discriminator; +use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; +use solana_account_decoder::UiAccountEncoding; +use solana_rpc_client_api::{ + config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; + +use crate::router::RouterState; + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct Tvl { + /// Vault Pubkey + vault_pubkey: String, + + /// Supported Token (JitoSOL, JTO...) + supported_mint: String, + + /// The amount of tokens deposited in Vault + native: u64, + usd: u64, +} + +/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. +/// +/// # Returns +/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. +/// +/// # Example +/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: +/// ``` +/// GET /validator_history/{vote_account}?epoch=200 +/// ``` +/// This request retrieves the history for the specified vote account, filtered by epoch 200. +pub(crate) async fn get_tvls( + State(state): State>, +) -> crate::Result { + let accounts = state + .rpc_client + .get_program_accounts_with_config( + &JITO_VAULT_ID, + RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new( + 0, + MemcmpEncodedBytes::Bytes(vec![jito_vault_core::vault::Vault::DISCRIMINATOR]), + ))]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + }, + ) + .await?; + + let mut tvls = Vec::new(); + for (vault_pubkey, vault) in accounts { + let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); + + tvls.push(Tvl { + vault_pubkey: vault_pubkey.to_string(), + supported_mint: vault.supported_mint.to_string(), + native: vault.tokens_deposited, + usd: 0, + }); + } + + Ok(Json(tvls)) +} From 86ac8f03c2361e14f946b4e1573ffd059ea9830a Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Wed, 29 Jan 2025 21:59:26 +0900 Subject: [PATCH 4/8] feat: fetch coingecko --- Cargo.lock | 103 ++++++++++++++++++ Cargo.toml | 1 + api/Cargo.toml | 1 + api/src/error.rs | 7 ++ api/src/lib.rs | 4 +- api/src/router/mod.rs | 5 +- api/src/router/vaults/get_vault.rs | 36 ------ api/src/router/vaults/mod.rs | 3 +- api/src/router/vaults/tvl.rs | 40 ++++--- .../vaults/{list_vaults.rs => vault.rs} | 38 ++++--- 10 files changed, 167 insertions(+), 71 deletions(-) delete mode 100644 api/src/router/vaults/get_vault.rs rename api/src/router/vaults/{list_vaults.rs => vault.rs} (63%) diff --git a/Cargo.lock b/Cargo.lock index bef8e02f..9f26be74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1843,6 +1843,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2240,6 +2255,19 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -2448,6 +2476,7 @@ dependencies = [ "jito-bytemuck", "jito-vault-client", "jito-vault-core", + "reqwest", "serde", "serde_json", "solana-account-decoder", @@ -3023,6 +3052,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.4" @@ -3265,12 +3311,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.17.0" @@ -3842,10 +3926,12 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -3857,6 +3943,7 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util 0.7.10", "tower-service", @@ -6377,6 +6464,16 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -6800,6 +6897,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 498cfcd6..dd99ad14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ num-derive = "0.4.2" num-traits = "0.2.19" proc-macro2 = "1.0.86" quote = "1.0.36" +reqwest = "0.11.27" serde = { version = "^1.0", features = ["derive"] } serde_json = "1.0.102" serde_with = "3.9.0" diff --git a/api/Cargo.toml b/api/Cargo.toml index c79c78bc..5e3dd092 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -21,6 +21,7 @@ http = { workspace = true } jito-bytemuck = { workspace = true } jito-vault-client = { workspace = true, features = ["serde"] } jito-vault-core = { workspace = true } +reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } solana-account-decoder = { workspace = true } diff --git a/api/src/error.rs b/api/src/error.rs index 9d1fa010..5c23a9a5 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -23,6 +23,9 @@ pub enum JitoRestakingApiError { #[error("Anchor Error")] AnchorError(#[from] anchor_lang::error::Error), + #[error("Reqwest Error")] + ReqwestError(#[from] reqwest::Error), + #[error("Internal Error")] InternalError, } @@ -47,6 +50,10 @@ impl IntoResponse for JitoRestakingApiError { error!("Anchor error: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") } + JitoRestakingApiError::ReqwestError(e) => { + error!("Reqwest error: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") + } JitoRestakingApiError::InternalError => { (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") } diff --git a/api/src/lib.rs b/api/src/lib.rs index 6507968d..f35bb7c8 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,6 +1,6 @@ use error::JitoRestakingApiError; -use serde::{Deserialize, Serialize}; -use solana_program::pubkey::Pubkey; +// use serde::{Deserialize, Serialize}; +// use solana_program::pubkey::Pubkey; pub mod error; pub mod router; diff --git a/api/src/router/mod.rs b/api/src/router/mod.rs index 22a541a2..4cdb43e9 100644 --- a/api/src/router/mod.rs +++ b/api/src/router/mod.rs @@ -16,7 +16,10 @@ use tower_http::{ LatencyUnit, }; use tracing::{info, instrument, Span}; -use vaults::{get_vault::get_vault, list_vaults::list_vaults, tvl::get_tvls}; +use vaults::{ + tvl::get_tvls, + vault::{get_vault, list_vaults}, +}; pub struct RouterState { pub rpc_client: RpcClient, diff --git a/api/src/router/vaults/get_vault.rs b/api/src/router/vaults/get_vault.rs deleted file mode 100644 index 3dfc6336..00000000 --- a/api/src/router/vaults/get_vault.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::{str::FromStr, sync::Arc}; - -use anchor_lang::{prelude::Pubkey, AnchorDeserialize}; -use axum::{ - extract::{Path, State}, - response::IntoResponse, - Json, -}; -use jito_vault_client::accounts::Vault; - -use crate::{error::JitoRestakingApiError, router::RouterState}; - -/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. -/// -/// # Returns -/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. -/// -/// # Example -/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: -/// ``` -/// GET /validator_history/{vote_account}?epoch=200 -/// ``` -/// This request retrieves the history for the specified vote account, filtered by epoch 200. -pub(crate) async fn get_vault( - State(state): State>, - Path(vault_pubkey): Path, -) -> crate::Result { - let vault_pubkey = Pubkey::from_str(&vault_pubkey)?; - let account = state.rpc_client.get_account(&vault_pubkey).await?; - let vault = Vault::deserialize(&mut account.data.as_slice()).map_err(|e| { - tracing::warn!("error deserializing Vault: {:?}", e); - JitoRestakingApiError::AnchorError(e.into()) - })?; - - Ok(Json(vault)) -} diff --git a/api/src/router/vaults/mod.rs b/api/src/router/vaults/mod.rs index eeccdfdf..6879f70a 100644 --- a/api/src/router/vaults/mod.rs +++ b/api/src/router/vaults/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod get_vault; -pub(crate) mod list_vaults; pub(crate) mod tvl; +pub(crate) mod vault; diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs index c8b62435..13f3c225 100644 --- a/api/src/router/vaults/tvl.rs +++ b/api/src/router/vaults/tvl.rs @@ -1,9 +1,10 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use anchor_lang::AnchorDeserialize; use axum::{extract::State, response::IntoResponse, Json}; use jito_bytemuck::Discriminator; use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; +use reqwest; use solana_account_decoder::UiAccountEncoding; use solana_rpc_client_api::{ config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, @@ -22,20 +23,11 @@ pub(crate) struct Tvl { /// The amount of tokens deposited in Vault native: u64, - usd: u64, + + /// The amount of tokens deposited in Vault in USD + usd: f64, } -/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. -/// -/// # Returns -/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. -/// -/// # Example -/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: -/// ``` -/// GET /validator_history/{vote_account}?epoch=200 -/// ``` -/// This request retrieves the history for the specified vote account, filtered by epoch 200. pub(crate) async fn get_tvls( State(state): State>, ) -> crate::Result { @@ -60,14 +52,34 @@ pub(crate) async fn get_tvls( .await?; let mut tvls = Vec::new(); + let mut price_tables = HashMap::new(); for (vault_pubkey, vault) in accounts { let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); + let price_usd = match price_tables.get(&vault.supported_mint.to_string()) { + Some(p) => *p, + None => { + let url = format!("https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses={}&vs_currencies=usd", vault.supported_mint); + let price_data: HashMap> = + reqwest::get(url).await?.json().await?; + + let mut p = 0f64; + if let Some(inner_map) = price_data.get(&vault.supported_mint.to_string()) { + if let Some(price) = inner_map.get("usd") { + p = *price; + } + } + + price_tables.insert(vault.supported_mint.to_string(), p); + p + } + }; + tvls.push(Tvl { vault_pubkey: vault_pubkey.to_string(), supported_mint: vault.supported_mint.to_string(), native: vault.tokens_deposited, - usd: 0, + usd: vault.tokens_deposited as f64 * price_usd, }); } diff --git a/api/src/router/vaults/list_vaults.rs b/api/src/router/vaults/vault.rs similarity index 63% rename from api/src/router/vaults/list_vaults.rs rename to api/src/router/vaults/vault.rs index 8b0ad27f..a39c4375 100644 --- a/api/src/router/vaults/list_vaults.rs +++ b/api/src/router/vaults/vault.rs @@ -1,7 +1,11 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; -use anchor_lang::AnchorDeserialize; -use axum::{extract::State, response::IntoResponse, Json}; +use anchor_lang::{prelude::Pubkey, AnchorDeserialize}; +use axum::{ + extract::{Path, State}, + response::IntoResponse, + Json, +}; use jito_bytemuck::Discriminator; use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; use solana_account_decoder::UiAccountEncoding; @@ -10,20 +14,8 @@ use solana_rpc_client_api::{ filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, }; -use crate::router::RouterState; - +use crate::{error::JitoRestakingApiError, router::RouterState}; -/// Retrieves the history of a specific validator, based on the provided vote account and optional epoch filter. -/// -/// # Returns -/// - `Ok(Json(history))`: A JSON response containing the validator history information. If the epoch filter is provided, it only returns the history for the specified epoch. -/// -/// # Example -/// This endpoint can be used to fetch the history of a validator's performance over time, either for a specific epoch or for all recorded epochs: -/// ``` -/// GET /validator_history/{vote_account}?epoch=200 -/// ``` -/// This request retrieves the history for the specified vote account, filtered by epoch 200. pub(crate) async fn list_vaults( State(state): State>, ) -> crate::Result { @@ -55,3 +47,17 @@ pub(crate) async fn list_vaults( Ok(Json(vaults)) } + +pub(crate) async fn get_vault( + State(state): State>, + Path(vault_pubkey): Path, +) -> crate::Result { + let vault_pubkey = Pubkey::from_str(&vault_pubkey)?; + let account = state.rpc_client.get_account(&vault_pubkey).await?; + let vault = Vault::deserialize(&mut account.data.as_slice()).map_err(|e| { + tracing::warn!("error deserializing Vault: {:?}", e); + JitoRestakingApiError::AnchorError(e.into()) + })?; + + Ok(Json(vault)) +} From 698cc3c1d0669b2b02729ea7a78a97aab36a78aa Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Wed, 29 Jan 2025 22:17:14 +0900 Subject: [PATCH 5/8] feat: use serde_json --- api/src/router/vaults/tvl.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs index 13f3c225..095e3dc1 100644 --- a/api/src/router/vaults/tvl.rs +++ b/api/src/router/vaults/tvl.rs @@ -57,21 +57,21 @@ pub(crate) async fn get_tvls( let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); let price_usd = match price_tables.get(&vault.supported_mint.to_string()) { - Some(p) => *p, + Some(p_usd) => *p_usd, None => { let url = format!("https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses={}&vs_currencies=usd", vault.supported_mint); - let price_data: HashMap> = - reqwest::get(url).await?.json().await?; + let response: serde_json::Value = reqwest::get(url).await?.json().await?; - let mut p = 0f64; - if let Some(inner_map) = price_data.get(&vault.supported_mint.to_string()) { - if let Some(price) = inner_map.get("usd") { - p = *price; - } - } + let p_usd = if let Some(price) = + response[vault.supported_mint.to_string()]["usd"].as_f64() + { + price + } else { + 0_f64 + }; - price_tables.insert(vault.supported_mint.to_string(), p); - p + price_tables.insert(vault.supported_mint.to_string(), p_usd); + p_usd } }; From 7c308be776a7879db7ecd16800cc6941a062e9c4 Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Fri, 31 Jan 2025 05:30:08 +0900 Subject: [PATCH 6/8] feat: use llama --- api/src/router/vaults/tvl.rs | 57 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs index 095e3dc1..36f1677f 100644 --- a/api/src/router/vaults/tvl.rs +++ b/api/src/router/vaults/tvl.rs @@ -1,10 +1,12 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use anchor_lang::AnchorDeserialize; use axum::{extract::State, response::IntoResponse, Json}; use jito_bytemuck::Discriminator; use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; -use reqwest; use solana_account_decoder::UiAccountEncoding; use solana_rpc_client_api::{ config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, @@ -28,6 +30,19 @@ pub(crate) struct Tvl { usd: f64, } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct CoinData { + decimals: u8, + price: f64, + symbol: String, + timestamp: f64, +} + +#[derive(Debug, serde::Deserialize)] +struct CoinResponse { + coins: HashMap, +} + pub(crate) async fn get_tvls( State(state): State>, ) -> crate::Result { @@ -51,28 +66,28 @@ pub(crate) async fn get_tvls( ) .await?; + let st_pubkeys: HashSet = accounts + .iter() + .map(|(_, vault)| { + let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); + vault.supported_mint.to_string() + }) + .collect(); + let st_pubkeys: Vec = st_pubkeys.into_iter().collect(); + + let base_url = String::from("https://coins.llama.fi/prices/current/solana:"); + let url = format!("{base_url}{}", st_pubkeys.join(",solana:").to_string()); + + let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); + let mut tvls = Vec::new(); - let mut price_tables = HashMap::new(); for (vault_pubkey, vault) in accounts { let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); - let price_usd = match price_tables.get(&vault.supported_mint.to_string()) { - Some(p_usd) => *p_usd, - None => { - let url = format!("https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses={}&vs_currencies=usd", vault.supported_mint); - let response: serde_json::Value = reqwest::get(url).await?.json().await?; - - let p_usd = if let Some(price) = - response[vault.supported_mint.to_string()]["usd"].as_f64() - { - price - } else { - 0_f64 - }; - - price_tables.insert(vault.supported_mint.to_string(), p_usd); - p_usd - } + let key = format!("solana:{}", vault.supported_mint.to_string()); + let price_usd = match response.coins.get(&key) { + Some(coin_data) => coin_data.price, + None => 0_f64, }; tvls.push(Tvl { @@ -83,5 +98,7 @@ pub(crate) async fn get_tvls( }); } + tvls.sort_by(|a, b| b.usd.total_cmp(&a.usd)); + Ok(Json(tvls)) } From 139b12589fa8c6559cd423a5ab0fe252c87c782c Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Fri, 31 Jan 2025 06:20:18 +0900 Subject: [PATCH 7/8] docs: README.md --- api/README.md | 291 +++++++++++++++-------------------- api/src/lib.rs | 104 ------------- api/src/router/mod.rs | 9 +- api/src/router/vaults/tvl.rs | 78 ++++++++-- 4 files changed, 197 insertions(+), 285 deletions(-) diff --git a/api/README.md b/api/README.md index 66c786a1..b86d25b9 100644 --- a/api/README.md +++ b/api/README.md @@ -4,213 +4,172 @@ ## Overview -The StakeNet API provides access to historical validator performance data on the Solana blockchain. This API can be useful for any website or application that needs to show validator performance history, specific epoch information, or the latest validator data. +This API allows users to: -## Getting started +- Retrieve TVL for each vault in native and USD vaules. +- List all vaults with their details. -### Prerequisites +## Endpoints -- [Solana's RPC Client](https://docs.rs/solana-rpc-client/latest/solana_rpc_client/) -- [Axum](https://docs.rs/axum/latest/axum/) +1. Get TVL for All Vaults -### Build for release +Fetches TVL for each vault in **native unit** and USD. -To build the API for release, run the following command: +Endpoint: -```bash -cargo b --release --bin jito-restaking-api +```http +GET api/v1/vaults/tvl ``` -### Check available options - -To view the options available for configuring the API: - -```bash -./target/release/jito-restaking-api --help - -# Usage: jito-stakenet-api [OPTIONS] -# -# Options: -# --bind-addr -# Bind address for the server [env: BIND_ADDR=] [default: 0.0.0.0:7001] -# --rpc-url -# RPC url [env: JSON_RPC_URL=] [default: https://api.mainnet-beta.solana.com] -# -h, --help -# Print help -# -V, --version -# Print version +Response: + +```json +[ + { + "vault_pubkey":"CugziSqZXcUStNPXbtRmq6atsrHqWY2fH2tAhsyApQrV", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "native_unit_tvl":291871.554825083, + "native_unit_symbol":"JITOSOL", + "usd_tvl":82042175.34578258 + }, + { + "vault_pubkey":"CQpvXgoaaawDCLh8FwMZEwQqnPakRUZ5BnzhjnEBPJv", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "native_unit_tvl":134310.541925135, + "native_unit_symbol":"JITOSOL", + "usd_tvl":37753350.229736194 + }, +] ``` -### Running the API - -Once built, run the API using the following command: - -```bash -./target/release/jito-stakenet-api -``` - -You can now send requests to http://localhost:7001 (or whichever address/port you specify in --bind-addr). - -## API Endpoints +2. Get TVL for a Specific Vault -|HTTP Method|Endpoint |Description | -|-----------|---------------------------------|----------------------| -|GET |/api/v1/vault/list |Fetch all vaults | +Fetches TVL for a single vault. +Endpoint: -### Example Requests +```http +GET api/v1/vaults/{vault_pubkey}/tvl/ +``` -#### Get all vaults: +Response: +```json +{ + "vault_pubkey":"CugziSqZXcUStNPXbtRmq6atsrHqWY2fH2tAhsyApQrV", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "native_unit_tvl":291871.554825083, + "native_unit_symbol":"JITOSOL", + "usd_tvl":82042175.34578258 +} ``` -curl http://localhost:7001/api/v1/vault/list -``` - -## Tips for Developers +3. List All Vaults -### Add a New Route +Retrieves all vaults with details. -If you want to add a new route to the router, you do it in `api/src/router.rs`: +Endpoint: -```rust -// api/src/router.rs +```http +GET api/v1/vaults +``` -let validator_history_routes = Router::new() - .route( - "/:vote_account", - get(get_all_validator_histories::get_all_validator_histories), - ) - .route( - "/:vote_account/latest", - get(get_latest_validator_history::get_latest_validator_history), - ) - .route( - "/:vote_account/new_route", - get(new_method), - ); +Response: + +```json +[ + { + "discriminator":2, + "base":"AbH5RtAgpnxyRuT9LqXR9ye4JuuJoHs6E5ENPvCnSRDk", + "vrt_mint":"CtJcH6BeUPKEfBNaUoPjmEc88E4aLuEJZU4NkuSdnpZo", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "vrt_supply":9306868868, + "tokens_deposited":9306868868, + "deposit_capacity":680411000000000, + "delegation_state": + { + "staked_amount":0, + "enqueued_for_cooldown_amount":0, + "cooling_down_amount":0, + ... + } + ... + } +... +] ``` -### Caching Validator History +4. Get Vault Details -You can implement a caching layer for the validator histories using the [Moka](https://docs.rs/moka/latest/moka/index.html) library. Here's an example of adding caching to the server. +Retrieves details of a specific vault. -#### Step 1: Add Moka Dependency +Endpoint: -```bash -cargo add moka --features future +```http +GET api/v1/vaults/{vault_pubkey} ``` -#### Step 2: Update State to Include Cache - -```rust -// api/src/router.rs - -pub struct RouterState { - pub validator_history_program_id: Pubkey, - pub rpc_client: RpcClient, - - // add cache - pub cache: Cache, +Response: + +```json +{ + "discriminator":2, + "base":"AbH5RtAgpnxyRuT9LqXR9ye4JuuJoHs6E5ENPvCnSRDk", + "vrt_mint":"CtJcH6BeUPKEfBNaUoPjmEc88E4aLuEJZU4NkuSdnpZo", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "vrt_supply":9306868868, + "tokens_deposited":9306868868, + "deposit_capacity":680411000000000, + "delegation_state": + { + "staked_amount":0, + "enqueued_for_cooldown_amount":0, + "cooling_down_amount":0, + ... + } + ... } ``` -#### Step 3: Modify Main To Use Cache -```rust -// api/src/bin/main.rs +## Build -#[tokio::main] -#[instrument] -async fn main() -> Result<(), Box> { - let args = Args::parse(); - - tracing_subscriber::fmt().init(); +### Prerequisites - info!("args: {:?}", args); +- [Solana's RPC Client](https://docs.rs/solana-rpc-client/latest/solana_rpc_client/) +- [Axum](https://docs.rs/axum/latest/axum/) - info!("starting server at {}", args.bind_addr); +### Build for release - let rpc_client = RpcClient::new(args.json_rpc_url.clone()); - info!("started rpc client at {}", args.json_rpc_url); +To build the API for release, run the following command: - // Create a cache that can store up to u64::MAX entries. - let cache: Cache = Cache::new(u64::MAX); +```bash +cargo b --release --bin jito-restaking-api +``` - let state = Arc::new(jito_stakenet_api::router::RouterState { - validator_history_program_id: args.validator_history_program_id, - rpc_client, - cache, - }); +### Check available options - let app = jito_stakenet_api::router::get_routes(state); +To view the options available for configuring the API: - axum::Server::bind(&args.bind_addr) - .serve(app.into_make_service_with_connect_info::()) - .await?; +```bash +./target/release/jito-restaking-api --help - Ok(()) -} +# Jito Restaking API +# +# Usage: jito-restaking-api [OPTIONS] +# +# Options: +# --bind-addr Bind address for the server [env: BIND_ADDR=] [default: 0.0.0.0:7001] +# --rpc-url RPC url [env: RPC_URL=] [default: https://api.mainnet-beta.solana.com] +# -h, --help Print help +# -V, --version Print version ``` -#### Step 4: Use Cache in Handlers - -```rust -// api/src/router/get_all_validator_histories.rs - -pub(crate) async fn get_all_validator_histories( - State(state): State>, - Path(vote_account): Path, - Query(epoch_query): Query, -) -> crate::Result { - let vote_account = Pubkey::from_str(&vote_account)?; - let history_account = - get_validator_history_address(&vote_account, &state.validator_history_program_id); - - // Check history_account's pubkey key in cache - match state.cache.get(&history_account).await { - Some(history) => Ok(Json(history)), - None => { - let account = state.rpc_client.get_account(&history_account).await?; - let validator_history = ValidatorHistory::try_deserialize(&mut account.data.as_slice()) - .map_err(|e| { - warn!("error deserializing ValidatorHistory: {:?}", e); - ApiError::ValidatorHistoryError("Error parsing ValidatorHistory".to_string()) - })?; - - let history_entries: Vec = match epoch_query.epoch { - Some(epoch) => validator_history - .history - .arr - .iter() - .filter_map(|entry| { - if epoch == entry.epoch { - Some(ValidatorHistoryEntryResponse::from_validator_history_entry( - entry, - )) - } else { - None - } - }) - .collect(), - None => validator_history - .history - .arr - .iter() - .map(ValidatorHistoryEntryResponse::from_validator_history_entry) - .collect(), - }; - - let history = ValidatorHistoryResponse::from_validator_history( - validator_history, - history_entries, - ); - - // Insert new history in cache - state.cache.insert(history_account, history.clone()).await; - - Ok(Json(history)) - } - } -} +### Running the API + +Once built, run the API using the following command: + +```bash +./target/release/jito-restaking-api -- --rpc-url "your-rpc-url" ``` + diff --git a/api/src/lib.rs b/api/src/lib.rs index f35bb7c8..1dfc5f1e 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,110 +1,6 @@ use error::JitoRestakingApiError; -// use serde::{Deserialize, Serialize}; -// use solana_program::pubkey::Pubkey; pub mod error; pub mod router; pub type Result = std::result::Result; - -// #[derive(Serialize, Deserialize)] -// pub(crate) struct ValidatorHistoryResponse { -// /// Cannot be enum due to Pod and Zeroable trait limitations -// pub(crate) struct_version: u32, -// -// pub(crate) vote_account: Pubkey, -// /// Index of validator of all ValidatorHistory accounts -// pub(crate) index: u32, -// -// /// These Crds gossip values are only signed and dated once upon startup and then never updated -// /// so we track latest time on-chain to make sure old messages aren't uploaded -// pub(crate) last_ip_timestamp: u64, -// pub(crate) last_version_timestamp: u64, -// -// pub(crate) history: Vec, -// } -// -// impl ValidatorHistoryResponse { -// pub fn from_validator_history( -// acc: ValidatorHistory, -// history_entries: Vec, -// ) -> Self { -// Self { -// struct_version: acc.struct_version, -// vote_account: acc.vote_account, -// index: acc.index, -// last_ip_timestamp: acc.last_ip_timestamp, -// last_version_timestamp: acc.last_version_timestamp, -// history: history_entries, -// } -// } -// } -// -// #[derive(Serialize, Deserialize)] -// pub(crate) struct ValidatorHistoryEntryResponse { -// pub(crate) activated_stake_lamports: u64, -// pub(crate) epoch: u16, -// -// // MEV commission in basis points -// pub(crate) mev_commission: u16, -// -// // Number of successful votes in current epoch. Not finalized until subsequent epoch -// pub(crate) epoch_credits: u32, -// -// // Validator commission in points -// pub(crate) commission: u8, -// -// // 0 if Solana Labs client, 1 if Jito client, >1 if other -// pub(crate) client_type: u8, -// pub(crate) version: ClientVersionResponse, -// pub(crate) ip: [u8; 4], -// -// // 0 if not a superminority validator, 1 if superminority validator -// pub(crate) is_superminority: u8, -// -// // rank of validator by stake amount -// pub(crate) rank: u32, -// -// // Most recent updated slot for epoch credits and commission -// pub(crate) vote_account_last_update_slot: u64, -// -// // MEV earned, stored as 1/100th SOL. mev_earned = 100 means 1.00 SOL earned -// pub(crate) mev_earned: u32, -// } - -// impl ValidatorHistoryEntryResponse { -// pub fn from_validator_history_entry(entry: &ValidatorHistoryEntry) -> Self { -// let version = ClientVersionResponse::from_client_version(entry.version); -// Self { -// activated_stake_lamports: entry.activated_stake_lamports, -// epoch: entry.epoch, -// mev_commission: entry.mev_commission, -// epoch_credits: entry.epoch_credits, -// commission: entry.commission, -// client_type: entry.client_type, -// version, -// ip: entry.ip, -// is_superminority: entry.is_superminority, -// rank: entry.rank, -// vote_account_last_update_slot: entry.vote_account_last_update_slot, -// mev_earned: entry.mev_earned, -// } -// } -// } -// -// #[derive(Serialize, Deserialize)] -// pub(crate) struct ClientVersionResponse { -// pub(crate) major: u8, -// pub(crate) minor: u8, -// pub(crate) patch: u16, -// } -// -// impl ClientVersionResponse { -// pub fn from_client_version(version: ClientVersion) -> Self { -// Self { -// major: version.major, -// minor: version.minor, -// patch: version.patch, -// } -// } -// } diff --git a/api/src/router/mod.rs b/api/src/router/mod.rs index 4cdb43e9..9d2ec289 100644 --- a/api/src/router/mod.rs +++ b/api/src/router/mod.rs @@ -17,7 +17,7 @@ use tower_http::{ }; use tracing::{info, instrument, Span}; use vaults::{ - tvl::get_tvls, + tvl::{get_tvl, get_tvls}, vault::{get_vault, list_vaults}, }; @@ -54,13 +54,14 @@ pub fn get_routes(state: Arc) -> Router { ); let vault_routes = Router::new() - .route("/list", get(list_vaults)) + .route("/", get(list_vaults)) + .route("/tvl", get(get_tvls)) .route("/:vault_pubkey", get(get_vault)) - .route("/tvls", get(get_tvls)); + .route("/:vault_pubkey/tvl", get(get_tvl)); let api_routes = Router::new() .route("/", get(root)) - .nest("/vault", vault_routes); + .nest("/vaults", vault_routes); let app = Router::new().nest("/api/v1", api_routes).fallback(fallback); diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs index 36f1677f..0ea4f9b4 100644 --- a/api/src/router/vaults/tvl.rs +++ b/api/src/router/vaults/tvl.rs @@ -1,10 +1,15 @@ use std::{ collections::{HashMap, HashSet}, + str::FromStr, sync::Arc, }; -use anchor_lang::AnchorDeserialize; -use axum::{extract::State, response::IntoResponse, Json}; +use anchor_lang::{prelude::Pubkey, AnchorDeserialize}; +use axum::{ + extract::{Path, State}, + response::IntoResponse, + Json, +}; use jito_bytemuck::Discriminator; use jito_vault_client::{accounts::Vault, programs::JITO_VAULT_ID}; use solana_account_decoder::UiAccountEncoding; @@ -13,7 +18,7 @@ use solana_rpc_client_api::{ filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, }; -use crate::router::RouterState; +use crate::{error::JitoRestakingApiError, router::RouterState}; #[derive(serde::Serialize, serde::Deserialize)] pub(crate) struct Tvl { @@ -24,10 +29,13 @@ pub(crate) struct Tvl { supported_mint: String, /// The amount of tokens deposited in Vault - native: u64, + native_unit_tvl: f64, + + /// Supported mint token symbol + native_unit_symbol: String, /// The amount of tokens deposited in Vault in USD - usd: f64, + usd_tvl: f64, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -85,20 +93,68 @@ pub(crate) async fn get_tvls( let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); let key = format!("solana:{}", vault.supported_mint.to_string()); - let price_usd = match response.coins.get(&key) { - Some(coin_data) => coin_data.price, - None => 0_f64, + let (native_unit_symbol, price_usd, decimals) = match response.coins.get(&key) { + Some(coin_data) => ( + coin_data.symbol.as_str(), + coin_data.price, + coin_data.decimals, + ), + None => ("", 0_f64, 0_u8), }; + let decimal_factor = 10u64.pow(decimals as u32) as f64; + let native_unit_tvl = vault.tokens_deposited as f64 / decimal_factor; tvls.push(Tvl { vault_pubkey: vault_pubkey.to_string(), supported_mint: vault.supported_mint.to_string(), - native: vault.tokens_deposited, - usd: vault.tokens_deposited as f64 * price_usd, + native_unit_tvl, + native_unit_symbol: native_unit_symbol.to_string(), + usd_tvl: native_unit_tvl * price_usd, }); } - tvls.sort_by(|a, b| b.usd.total_cmp(&a.usd)); + tvls.sort_by(|a, b| b.usd_tvl.total_cmp(&a.usd_tvl)); Ok(Json(tvls)) } + +pub(crate) async fn get_tvl( + State(state): State>, + Path(vault_pubkey): Path, +) -> crate::Result { + let vault_pubkey = Pubkey::from_str(&vault_pubkey)?; + + let account = state.rpc_client.get_account(&vault_pubkey).await?; + let vault = Vault::deserialize(&mut account.data.as_slice()).map_err(|e| { + tracing::warn!("error deserializing Vault: {:?}", e); + JitoRestakingApiError::AnchorError(e.into()) + })?; + + let url = format!( + "https://coins.llama.fi/prices/current/solana:{}", + vault.supported_mint.to_string(), + ); + let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); + + let key = format!("solana:{}", vault.supported_mint.to_string()); + let (native_unit_symbol, price_usd, decimals) = match response.coins.get(&key) { + Some(coin_data) => ( + coin_data.symbol.as_str(), + coin_data.price, + coin_data.decimals, + ), + None => ("", 0_f64, 0_u8), + }; + + let decimal_factor = 10u64.pow(decimals as u32) as f64; + let native_unit_tvl = vault.tokens_deposited as f64 / decimal_factor; + let tvl = Tvl { + vault_pubkey: vault_pubkey.to_string(), + supported_mint: vault.supported_mint.to_string(), + native_unit_tvl, + native_unit_symbol: native_unit_symbol.to_string(), + usd_tvl: native_unit_tvl * price_usd, + }; + + Ok(Json(tvl)) +} From 23e6507cc752a599aeaa3b4766222f70a4d8e110 Mon Sep 17 00:00:00 2001 From: aoikurokawa Date: Fri, 31 Jan 2025 06:38:02 +0900 Subject: [PATCH 8/8] chore: make lint happy --- api/src/error.rs | 65 ++++++++++++++++++++-------------- api/src/router/vaults/mod.rs | 4 +-- api/src/router/vaults/tvl.rs | 54 +++++++++++++++------------- api/src/router/vaults/vault.rs | 4 +-- 4 files changed, 72 insertions(+), 55 deletions(-) diff --git a/api/src/error.rs b/api/src/error.rs index 5c23a9a5..c4211f29 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -12,6 +12,11 @@ use solana_rpc_client_api::client_error::Error as RpcError; use thiserror::Error; use tracing::error; +#[derive(Debug, Serialize, Deserialize)] +pub struct Error { + pub error: String, +} + #[derive(Error, Debug)] pub enum JitoRestakingApiError { #[error("Rpc Error")] @@ -30,34 +35,13 @@ pub enum JitoRestakingApiError { InternalError, } -#[derive(Debug, Serialize, Deserialize)] -pub struct Error { - pub error: String, -} - impl IntoResponse for JitoRestakingApiError { fn into_response(self) -> Response { - let (status, error_message) = match self { - JitoRestakingApiError::RpcError(e) => { - error!("Rpc error: {e}"); - (StatusCode::INTERNAL_SERVER_ERROR, "Rpc error") - } - JitoRestakingApiError::ParsePubkeyError(e) => { - error!("Parse pubkey error: {e}"); - (StatusCode::INTERNAL_SERVER_ERROR, "Pubkey parse error") - } - JitoRestakingApiError::AnchorError(e) => { - error!("Anchor error: {e}"); - (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") - } - JitoRestakingApiError::ReqwestError(e) => { - error!("Reqwest error: {e}"); - (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") - } - JitoRestakingApiError::InternalError => { - (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") - } - }; + let status = StatusCode::INTERNAL_SERVER_ERROR; + let error_message = self.get_error_message(); + + self.log_error(); + ( status, Json(Error { @@ -68,6 +52,35 @@ impl IntoResponse for JitoRestakingApiError { } } +impl JitoRestakingApiError { + /// Helper function to map errors to their message + const fn get_error_message(&self) -> &'static str { + match self { + Self::RpcError(_) => "Rpc error", + Self::ParsePubkeyError(_) => "Pubkey parse error", + Self::AnchorError(_) | Self::ReqwestError(_) | Self::InternalError => { + "Internal Server Error" + } + } + } + + /// Logs the error, extracting the error details separately + fn log_error(&self) { + match self { + Self::RpcError(e) => self.log("Rpc error", e), + Self::ParsePubkeyError(e) => self.log("Parse pubkey error", e), + Self::AnchorError(e) => self.log("Anchor error", e), + Self::ReqwestError(e) => self.log("Reqwest error", e), + Self::InternalError => self.log("Internal server error", ""), + } + } + + /// Helper function to log messages + fn log(&self, prefix: &str, err: T) { + error!("{}: {}", prefix, err); + } +} + pub async fn handle_error(error: BoxError) -> Result { if error.is::() { return Ok(( diff --git a/api/src/router/vaults/mod.rs b/api/src/router/vaults/mod.rs index 6879f70a..e23d83b4 100644 --- a/api/src/router/vaults/mod.rs +++ b/api/src/router/vaults/mod.rs @@ -1,2 +1,2 @@ -pub(crate) mod tvl; -pub(crate) mod vault; +pub mod tvl; +pub mod vault; diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs index 0ea4f9b4..b7e1efae 100644 --- a/api/src/router/vaults/tvl.rs +++ b/api/src/router/vaults/tvl.rs @@ -21,7 +21,7 @@ use solana_rpc_client_api::{ use crate::{error::JitoRestakingApiError, router::RouterState}; #[derive(serde::Serialize, serde::Deserialize)] -pub(crate) struct Tvl { +pub struct Tvl { /// Vault Pubkey vault_pubkey: String, @@ -51,9 +51,7 @@ struct CoinResponse { coins: HashMap, } -pub(crate) async fn get_tvls( - State(state): State>, -) -> crate::Result { +pub async fn get_tvls(State(state): State>) -> crate::Result { let accounts = state .rpc_client .get_program_accounts_with_config( @@ -84,7 +82,7 @@ pub(crate) async fn get_tvls( let st_pubkeys: Vec = st_pubkeys.into_iter().collect(); let base_url = String::from("https://coins.llama.fi/prices/current/solana:"); - let url = format!("{base_url}{}", st_pubkeys.join(",solana:").to_string()); + let url = format!("{base_url}{}", st_pubkeys.join(",solana:")); let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); @@ -92,15 +90,18 @@ pub(crate) async fn get_tvls( for (vault_pubkey, vault) in accounts { let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); - let key = format!("solana:{}", vault.supported_mint.to_string()); - let (native_unit_symbol, price_usd, decimals) = match response.coins.get(&key) { - Some(coin_data) => ( - coin_data.symbol.as_str(), - coin_data.price, - coin_data.decimals, - ), - None => ("", 0_f64, 0_u8), - }; + let key = format!("solana:{}", vault.supported_mint); + let (native_unit_symbol, price_usd, decimals) = + response + .coins + .get(&key) + .map_or(("", 0_f64, 0_u8), |coin_data| { + ( + coin_data.symbol.as_str(), + coin_data.price, + coin_data.decimals, + ) + }); let decimal_factor = 10u64.pow(decimals as u32) as f64; let native_unit_tvl = vault.tokens_deposited as f64 / decimal_factor; @@ -118,7 +119,7 @@ pub(crate) async fn get_tvls( Ok(Json(tvls)) } -pub(crate) async fn get_tvl( +pub async fn get_tvl( State(state): State>, Path(vault_pubkey): Path, ) -> crate::Result { @@ -132,19 +133,22 @@ pub(crate) async fn get_tvl( let url = format!( "https://coins.llama.fi/prices/current/solana:{}", - vault.supported_mint.to_string(), + vault.supported_mint, ); let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); - let key = format!("solana:{}", vault.supported_mint.to_string()); - let (native_unit_symbol, price_usd, decimals) = match response.coins.get(&key) { - Some(coin_data) => ( - coin_data.symbol.as_str(), - coin_data.price, - coin_data.decimals, - ), - None => ("", 0_f64, 0_u8), - }; + let key = format!("solana:{}", vault.supported_mint); + let (native_unit_symbol, price_usd, decimals) = + response + .coins + .get(&key) + .map_or(("", 0_f64, 0_u8), |coin_data| { + ( + coin_data.symbol.as_str(), + coin_data.price, + coin_data.decimals, + ) + }); let decimal_factor = 10u64.pow(decimals as u32) as f64; let native_unit_tvl = vault.tokens_deposited as f64 / decimal_factor; diff --git a/api/src/router/vaults/vault.rs b/api/src/router/vaults/vault.rs index a39c4375..720c7e3b 100644 --- a/api/src/router/vaults/vault.rs +++ b/api/src/router/vaults/vault.rs @@ -16,7 +16,7 @@ use solana_rpc_client_api::{ use crate::{error::JitoRestakingApiError, router::RouterState}; -pub(crate) async fn list_vaults( +pub async fn list_vaults( State(state): State>, ) -> crate::Result { let accounts = state @@ -48,7 +48,7 @@ pub(crate) async fn list_vaults( Ok(Json(vaults)) } -pub(crate) async fn get_vault( +pub async fn get_vault( State(state): State>, Path(vault_pubkey): Path, ) -> crate::Result {