diff --git a/Cargo.lock b/Cargo.lock index 338996ba..9f26be74 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" @@ -1794,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" @@ -2129,6 +2193,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" @@ -2185,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" @@ -2382,6 +2465,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "jito-restaking-api" +version = "0.0.3" +dependencies = [ + "anchor-lang", + "axum", + "clap 4.5.16", + "http", + "jito-bytemuck", + "jito-vault-client", + "jito-vault-core", + "reqwest", + "serde", + "serde_json", + "solana-account-decoder", + "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 +2911,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" @@ -2927,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" @@ -2956,6 +3098,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" @@ -3159,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" @@ -3213,6 +3403,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 +3873,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 +3894,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" @@ -3715,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", @@ -3730,6 +3943,7 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util 0.7.10", "tower-service", @@ -4022,6 +4236,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" @@ -6240,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" @@ -6386,6 +6620,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 +6701,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 +6731,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]] @@ -6603,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 cf2ac2b6..dd99ad14 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" } @@ -69,7 +72,9 @@ 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" shank = "0.4.2" shank_idl = "0.4.2" @@ -89,3 +94,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..5e3dd092 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,37 @@ +[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 } +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 } +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..b86d25b9 --- /dev/null +++ b/api/README.md @@ -0,0 +1,175 @@ +# Jito Restaking API + +# StakeNet API + +## Overview + +This API allows users to: + +- Retrieve TVL for each vault in native and USD vaules. +- List all vaults with their details. + +## Endpoints + +1. Get TVL for All Vaults + +Fetches TVL for each vault in **native unit** and USD. + +Endpoint: + +```http +GET api/v1/vaults/tvl +``` + +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 + }, +] +``` + +2. Get TVL for a Specific Vault + +Fetches TVL for a single vault. + +Endpoint: + +```http +GET api/v1/vaults/{vault_pubkey}/tvl/ +``` + +Response: + +```json +{ + "vault_pubkey":"CugziSqZXcUStNPXbtRmq6atsrHqWY2fH2tAhsyApQrV", + "supported_mint":"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "native_unit_tvl":291871.554825083, + "native_unit_symbol":"JITOSOL", + "usd_tvl":82042175.34578258 +} +``` + +3. List All Vaults + +Retrieves all vaults with details. + +Endpoint: + +```http +GET api/v1/vaults +``` + +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, + ... + } + ... + } +... +] +``` + +4. Get Vault Details + +Retrieves details of a specific vault. + +Endpoint: + +```http +GET api/v1/vaults/{vault_pubkey} +``` + +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, + ... + } + ... +} +``` + + +## Build + +### 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 + +# 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 +``` + +### 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/bin/main.rs b/api/src/bin/main.rs new file mode 100644 index 00000000..f50c7466 --- /dev/null +++ b/api/src/bin/main.rs @@ -0,0 +1,42 @@ +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..c4211f29 --- /dev/null +++ b/api/src/error.rs @@ -0,0 +1,111 @@ +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(Debug, Serialize, Deserialize)] +pub struct Error { + pub error: String, +} + +#[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("Reqwest Error")] + ReqwestError(#[from] reqwest::Error), + + #[error("Internal Error")] + InternalError, +} + +impl IntoResponse for JitoRestakingApiError { + fn into_response(self) -> Response { + let status = StatusCode::INTERNAL_SERVER_ERROR; + let error_message = self.get_error_message(); + + self.log_error(); + + ( + status, + Json(Error { + error: error_message.to_string(), + }), + ) + .into_response() + } +} + +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(( + 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 new file mode 100644 index 00000000..1dfc5f1e --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,6 @@ +use error::JitoRestakingApiError; + +pub mod error; +pub mod router; + +pub type Result = std::result::Result; diff --git a/api/src/router/mod.rs b/api/src/router/mod.rs new file mode 100644 index 00000000..9d2ec289 --- /dev/null +++ b/api/src/router/mod.rs @@ -0,0 +1,77 @@ +mod vaults; + +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 vaults::{ + tvl::{get_tvl, get_tvls}, + vault::{get_vault, 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("/", get(list_vaults)) + .route("/tvl", get(get_tvls)) + .route("/:vault_pubkey", get(get_vault)) + .route("/:vault_pubkey/tvl", get(get_tvl)); + + let api_routes = Router::new() + .route("/", get(root)) + .nest("/vaults", 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/vaults/mod.rs b/api/src/router/vaults/mod.rs new file mode 100644 index 00000000..e23d83b4 --- /dev/null +++ b/api/src/router/vaults/mod.rs @@ -0,0 +1,2 @@ +pub mod tvl; +pub mod vault; diff --git a/api/src/router/vaults/tvl.rs b/api/src/router/vaults/tvl.rs new file mode 100644 index 00000000..b7e1efae --- /dev/null +++ b/api/src/router/vaults/tvl.rs @@ -0,0 +1,164 @@ +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, + sync::Arc, +}; + +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; +use solana_rpc_client_api::{ + config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; + +use crate::{error::JitoRestakingApiError, router::RouterState}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Tvl { + /// Vault Pubkey + vault_pubkey: String, + + /// Supported Token (JitoSOL, JTO...) + supported_mint: String, + + /// The amount of tokens deposited in Vault + native_unit_tvl: f64, + + /// Supported mint token symbol + native_unit_symbol: String, + + /// The amount of tokens deposited in Vault in USD + usd_tvl: 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 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 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:")); + + let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); + + let mut tvls = Vec::new(); + for (vault_pubkey, vault) in accounts { + let vault = Vault::deserialize(&mut vault.data.as_slice()).unwrap(); + + 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; + tvls.push(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, + }); + } + + tvls.sort_by(|a, b| b.usd_tvl.total_cmp(&a.usd_tvl)); + + Ok(Json(tvls)) +} + +pub 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, + ); + let response: CoinResponse = reqwest::get(url).await.unwrap().json().await.unwrap(); + + 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; + 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)) +} diff --git a/api/src/router/vaults/vault.rs b/api/src/router/vaults/vault.rs new file mode 100644 index 00000000..720c7e3b --- /dev/null +++ b/api/src/router/vaults/vault.rs @@ -0,0 +1,63 @@ +use std::{str::FromStr, sync::Arc}; + +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; +use solana_rpc_client_api::{ + config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; + +use crate::{error::JitoRestakingApiError, router::RouterState}; + +pub 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)) +} + +pub 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)) +}