diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..99fc5c1 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATAMAXI_API_KEY= diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..2ad40f2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +name: Publish Documentation + +on: + push: + branches: [main] + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Configure cache + uses: Swatinem/rust-cache@v2 + + - name: Setup pages + id: pages + uses: actions/configure-pages@v4 + + - name: Clean docs folder + run: cargo clean --doc + + - name: Build docs + run: cargo doc --no-deps + + - name: Add redirect + run: echo '' > target/doc/index.html + + - name: Remove lock file + run: rm target/doc/.lock + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + publish_branch: gh-pages + allow_empty_commit: true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..87e3d0a --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,38 @@ +name: Rust + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: clippy + - run: cargo clippy --all-targets + + fmt: + name: format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + profile: minimal + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5879244 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +src/main.rs + +Cargo.lock +.env diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..10a095a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "datamaxi" +version = "0.1.0" +edition = "2021" +readme = "README.md" +license = "MIT" + +[dependencies] +dotenv = "0.15.0" +api = "0.2.0" +error-chain = "0.12.4" +reqwest = { version = "0.12.7", features = ["blocking", "json"] } +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.127" +tungstenite = "0.24.0" +url = "2.5.2" + +[dev-dependencies] +mockito = "1.5.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5a818ba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Bisonai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79b8936 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +## DataMaxi+ Rust SDK + +This is the official implementation of Rust SDK for [DataMaxi+](https://datamaxiplus.com/). +The package can be used to fetch both historical and latest data using [DataMaxi+ API](https://docs.datamaxiplus.com/). + +- [Installation](#installation) +- [Configuration](#configuration) +- [Links](#links) +- [Contributing](#contributing) +- [License](#license) + +### Installation + +```shell +[dependencies] +datamaxi = { git = "https://github.com/bisonai/datamaxi-rust.git" } +``` + +### Configuration + +Private API endpoints are protected by an API key. +You can get the API key upon registering at . + + +| Option | Explanation | +|------------|-------------------------------------------------------------------------------| +| `api_key` | Your API key | +| `base_url` | If `base_url` is not provided, it defaults to `https://api.datamaxiplus.com`. | + +### Examples + +#### CEX Candle + +```rust +let api_key = "my_api_key".to_string(); +let candle: datamaxi::cex::Candle = datamaxi::api::Datamaxi::new(api_key); + +// Fetch supported exchanges for CEX candle data +candle.exchanges("spot"); + +// Fetch supported symbols for CEX candle data +let symbols_options = datamaxi::cex::SymbolsOptions::new(); +candle.symbols("binance", symbols_options); + +// Fetch supported intervals for CEX candle data +candle.intervals(); + +// Fetch CEX candle data +let candle_options = datamaxi::cex::CandleOptions::new(); +candle.get("binance", "ETH-USDT", candle_options); +``` + +#### DEX Candle + +```rust +let api_key = "my_api_key".to_string(); +let candle: datamaxi::dex::Candle = datamaxi::api::Datamaxi::new(api_key); + +// Fetch supported intervals for DEX candle data +candle.intervals(); + +// Fetch supported exchange for DEX candle data +candle.exchanges(); + +// Fetch supported chains for DEX candle data +candle.chains(); + +// Fetch supported pools for DEX candle data +let pools_options = datamaxi::dex::PoolsOptions::new(); +candle.pools(pools_options); + +// Fetch DEX candle data +let params = datamaxi::dex::CandleOptions::new(); +candle.get( + "bsc_mainnet", + "pancakeswap", + "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", + params, +); +``` + +#### DEX Trade + +```rust +let api_key = "my_api_key".to_string(); +let trade: datamaxi::dex::Trade = datamaxi::api::Datamaxi::new(api_key); + +// Fetch supported exchange for DEX trade data +trade.exchanges(); + +// Fetch supported chains for DEX trade data +trade.chains(); + +// Fetch supported pools for DEX trade data +let pools_options = datamaxi::dex::PoolsOptions::new(); +trade.pools(pools_options); + +// Fetch DEX candle data +let trade_options = datamaxi::dex::TradeOptions::new().limit(5); +trade.get( + "bsc_mainnet", + "pancakeswap", + "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", + trade_options +); +``` + +### Links + +- [Official Website](https://datamaxiplus.com/) +- [Documentation](https://docs.datamaxiplus.com/) + +### Contributing + +We welcome contributions! +If you discover a bug in this project, please feel free to open an issue to discuss the changes you would like to propose. + +### License + +[MIT License](./LICENSE) diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..274bb44 --- /dev/null +++ b/README.tpl @@ -0,0 +1 @@ +{{readme}} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..fee0038 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +# DataMaxi+ Examples + +All examples load `DATAMAXI_API_KEY` environment variable to authenticate with the DataMaxi+ API. +Setup your environment variable with your API key in `.env` file before running the examples. + +## CEX Candle + +```bash +cargo run --release -q --example "cex-candle" +``` + +## DEX Trade + +```bash +cargo run --release -q --example "dex-trade" +``` + +## DEX Candle + +```bash +cargo run --release -q --example "dex-candle" +``` diff --git a/examples/cex-candle.rs b/examples/cex-candle.rs new file mode 100644 index 0000000..02a1d8d --- /dev/null +++ b/examples/cex-candle.rs @@ -0,0 +1,41 @@ +use std::env; + +fn main() { + dotenv::dotenv().ok(); + let api_key = env::var("DATAMAXI_API_KEY").expect("DATAMAXI_API_KEY not found"); + let candle: datamaxi::cex::Candle = datamaxi::api::Datamaxi::new(api_key); + + // CEX Candle Exchanges + match candle.exchanges("futures") { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // CEX Candle Symbols + let symbols_options = datamaxi::cex::SymbolsOptions::new(); + let symbols_response = candle.symbols("binance", symbols_options); + match symbols_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } + + // CEX Candle Intervals + match candle.intervals() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // CEX Candle Data + let candle_options = datamaxi::cex::CandleOptions::new(); + let candle_response = candle.get("binance", "ETH-USDT", candle_options); + match candle_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } +} diff --git a/examples/dex-candle.rs b/examples/dex-candle.rs new file mode 100644 index 0000000..718d7b0 --- /dev/null +++ b/examples/dex-candle.rs @@ -0,0 +1,52 @@ +use std::env; + +fn main() { + dotenv::dotenv().ok(); + let api_key = env::var("DATAMAXI_API_KEY").expect("DATAMAXI_API_KEY not found"); + let candle: datamaxi::dex::Candle = datamaxi::api::Datamaxi::new(api_key); + + // DEX Candle Intervals + match candle.intervals() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // DEX Candle Exchanges + match candle.exchanges() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // DEX Candle Chains + match candle.chains() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // DEX Candle Pools + let pools_options = datamaxi::dex::PoolsOptions::new(); + let pools_response = candle.pools(pools_options); + match pools_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } + + // DEX Candle Data + let params = datamaxi::dex::CandleOptions::new(); + let candle_response = candle.get( + "bsc_mainnet", + "pancakeswap", + "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", + params, + ); + match candle_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } +} diff --git a/examples/dex-trade.rs b/examples/dex-trade.rs new file mode 100644 index 0000000..fa0a58c --- /dev/null +++ b/examples/dex-trade.rs @@ -0,0 +1,46 @@ +use std::env; + +fn main() { + dotenv::dotenv().ok(); + let api_key = env::var("DATAMAXI_API_KEY").expect("DATAMAXI_API_KEY not found"); + let trade: datamaxi::dex::Trade = datamaxi::api::Datamaxi::new(api_key); + + // DEX Trade Exchanges + match trade.exchanges() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // DEX Trade Chains + match trade.chains() { + Ok(answer) => println!("{:?}", answer), + Err(e) => println!("Error: {}", e), + } + + // DEX Trade Pools + let pools_options = datamaxi::dex::PoolsOptions::new(); + let pools_response = trade.pools(pools_options); + match pools_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } + + // DEX Trade Data + let trade_options = datamaxi::dex::TradeOptions::new().limit(5); + let trade_response = trade.get( + "bsc_mainnet", + "pancakeswap", + "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", + trade_options, + ); + match trade_response { + Ok(answer) => match serde_json::to_string(&answer) { + Ok(json) => println!("{}", json), + Err(e) => println!("Error: {}", e), + }, + Err(e) => println!("Error: {}", e), + } +} diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..4d203c3 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,140 @@ +use error_chain::error_chain; +use reqwest::blocking::Response; +use reqwest::StatusCode; +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::io::Read; + +const BASE_URL: &str = "https://api.datamaxiplus.com/api/v1"; + +/// A trait that defines the required methods for interacting with the Datamaxi+ API. +pub trait Datamaxi { + /// Creates a new instance of the implementing type using the provided API key. + fn new(api_key: String) -> Self; + + /// Creates a new instance of the implementing type using the provided API key and base URL. + fn new_with_base_url(api_key: String, base_url: String) -> Self; +} + +/// The configuration for the Datamaxi+ API client. +pub struct Config { + /// The base URL for the API. + pub base_url: Option, + + /// The API key used for authentication. + pub api_key: String, +} + +/// The client for interacting with the Datamaxi+ API. +#[derive(Clone)] +pub struct Client { + base_url: String, + api_key: String, + inner_client: reqwest::blocking::Client, +} + +impl Client { + /// Creates a new instance of the `Client` struct with the provided configuration. + pub fn new(config: Config) -> Self { + Client { + base_url: config.base_url.unwrap_or(BASE_URL.to_string()), + api_key: config.api_key, + inner_client: reqwest::blocking::Client::builder() + .pool_idle_timeout(None) + .build() + .unwrap(), + } + } + + /// Builds a request string from a set of parameters. + fn build_request(parameters: HashMap) -> String { + parameters + .iter() + .map(|(key, value)| format!("{}={}", key, value)) + .collect::>() + .join("&") + } + + /// Sends a GET request to the specified endpoint with optional parameters. + pub fn get( + &self, + endpoint: &'static str, + parameters: Option>, + ) -> Result { + let mut url: String = format!("{}{}", self.base_url, endpoint); + + if let Some(p) = parameters { + let request = Self::build_request(p); + + if !request.is_empty() { + url.push_str(format!("?{}", request).as_str()); + } + } + + let client = &self.inner_client; + let response = client + .get(url.as_str()) + .header("X-DTMX-APIKEY", &self.api_key) + .send()?; + + self.handle_response(response) + } + + /// Processes the response from the API and returns the result. + fn handle_response(&self, response: Response) -> Result { + match response.status() { + StatusCode::OK => Ok(response.json::()?), + StatusCode::INTERNAL_SERVER_ERROR => { + let mut response_text = String::new(); + response.take(1000).read_to_string(&mut response_text)?; + Err(ErrorKind::InternalServerError(response_text).into()) + } + StatusCode::UNAUTHORIZED => Err(ErrorKind::Unauthorized.into()), + StatusCode::BAD_REQUEST => { + let mut response_text = String::new(); + response.take(1000).read_to_string(&mut response_text)?; + Err(ErrorKind::BadRequest(response_text).into()) + } + status => Err(ErrorKind::UnexpectedStatusCode(status.as_u16()).into()), + } + } +} + +error_chain! { + errors { + /// Represents an error that occurs when a request to the API returns a bad request status. + BadRequest(msg: String) { + description("Bad request") + display("Bad request: {}", msg) + } + + /// Represents an error that occurs when a request to the API returns an unauthorized status. + Unauthorized { + description("Unauthorized") + display("Unauthorized") + } + + /// Represents an error that occurs when a request to the API returns an internal server error status. + InternalServerError(msg: String) { + description("Internal server error") + display("Internal server error: {}", msg) + } + + /// Represents an error that occurs when a request to the API returns an unexpected status code. + UnexpectedStatusCode(status: u16) { + description("Unexpected status code") + display("Received unexpected status code: {}", status) + } + } + + foreign_links { + ReqError(reqwest::Error); + InvalidHeaderError(reqwest::header::InvalidHeaderValue); + IoError(std::io::Error); + ParseFloatError(std::num::ParseFloatError); + UrlParserError(url::ParseError); + Json(serde_json::Error); + Tungstenite(tungstenite::Error); + TimestampError(std::time::SystemTimeError); + } +} diff --git a/src/cex.rs b/src/cex.rs new file mode 100644 index 0000000..81d1254 --- /dev/null +++ b/src/cex.rs @@ -0,0 +1,150 @@ +use crate::api::{Client, Config, Datamaxi, Result}; +pub use crate::models::{CandleOptions, SymbolsOptions}; +use crate::models::{CandleResponse, SymbolsResponse}; +use std::collections::HashMap; + +/// Provides methods for retrieving CEX candle data and related information. +#[derive(Clone)] +pub struct Candle { + pub client: Client, +} + +impl Candle { + /// Retrieves candle data for a specified exchange and symbol. Additional parameters can be + /// provided to filter and sort the results. The response will contain an array of candle data + /// objects, each representing a single candle with open, high, low, close, and volume values. + pub fn get( + &self, + exchange: E, + symbol: S, + options: CandleOptions, + ) -> Result + where + E: Into, + S: Into, + { + let mut parameters = HashMap::new(); + + // required + parameters.insert("exchange".to_string(), exchange.into()); + parameters.insert("symbol".to_string(), symbol.into()); + + // optional + parameters.extend( + [ + options + .market + .map(|market| ("market".to_string(), market.to_string())), + options + .interval + .map(|interval| ("interval".to_string(), interval.to_string())), + options + .page + .map(|page| ("page".to_string(), page.to_string())), + options + .limit + .map(|limit| ("limit".to_string(), limit.to_string())), + options + .from + .map(|from| ("from".to_string(), from.to_string())), + options.to.map(|to| ("to".to_string(), to.to_string())), + options + .sort + .map(|sort| ("sort".to_string(), sort.to_string())), + ] + .into_iter() + .flatten(), + ); + + self.client.get("/cex/candle", Some(parameters)) + } + + /// Retrieves a list of supported exchanges for candle data. The market parameter can be used + /// to filter the results by market. + pub fn exchanges(&self, market: M) -> Result> + where + M: Into, + { + let mut parameters = HashMap::new(); + + // required + parameters.insert("market".to_string(), market.into()); + + self.client.get("/cex/candle/exchanges", Some(parameters)) + } + + /// Retrieves a list of supported symbols for candle data. The exchange parameter is required, + /// and the market parameter can be used to filter the results by market. + pub fn symbols(&self, exchange: E, options: SymbolsOptions) -> Result> + where + E: Into, + { + let mut parameters = HashMap::new(); + + // required + parameters.insert("exchange".to_string(), exchange.into()); + + // optional + if let Some(market) = options.market { + parameters.insert("market".to_string(), market); + } + + self.client.get("/cex/candle/symbols", Some(parameters)) + } + + /// Retrieves a list of supported candle intervals. + pub fn intervals(&self) -> Result> { + self.client.get("/cex/candle/intervals", None) + } +} + +/// Implements the `Datamaxi` trait for `Candle`, providing methods +/// to create new instances of `Candle` with or without a custom base URL. +impl Datamaxi for Candle { + /// Creates a new `Candle` instance with the default base URL. + /// + /// # Parameters + /// - `api_key`: A `String` representing the API key used for authentication in API requests. + /// + /// # Returns + /// A new `Candle` instance configured with the default base URL and the provided `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let candle = datamaxi::dex::Candle::new("my_api_key".to_string()); + /// ``` + fn new(api_key: String) -> Candle { + let config = Config { + base_url: None, // Default base URL will be used + api_key, // Provided API key + }; + Candle { + client: Client::new(config), // Create a new client with the given config + } + } + + /// Creates a new `Candle` instance with a custom base URL. + /// + /// # Parameters + /// - `api_key`: A `String` representing the API key used for authentication in API requests. + /// - `base_url`: A `String` representing the custom base URL for API requests. + /// + /// # Returns + /// A new `Candle` instance configured with the specified `base_url` and `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let candle = datamaxi::dex::Candle::new_with_base_url("my_api_key".to_string(), "https://custom-api.example.com".to_string()); + /// ``` + fn new_with_base_url(api_key: String, base_url: String) -> Candle { + let config = Config { + base_url: Some(base_url), // Use the provided custom base URL + api_key, // Provided API key + }; + Candle { + client: Client::new(config), // Create a new client with the given config + } + } +} diff --git a/src/dex.rs b/src/dex.rs new file mode 100644 index 0000000..240c1cc --- /dev/null +++ b/src/dex.rs @@ -0,0 +1,291 @@ +use crate::api::{Client, Config, Datamaxi, Result}; +pub use crate::models::{CandleOptions, PoolsOptions, TradeOptions}; +use crate::models::{CandleResponse, PoolsResponse, TradeResponse}; +use std::collections::HashMap; + +/// Provides methods for retrieving DEX candle data and related information. +#[derive(Clone)] +pub struct Candle { + pub client: Client, +} + +impl Candle { + /// Retrieves candle data for a specified chain, exchange, and pool. Additional parameters can be + /// provided to filter and sort the results. The response will contain an array of candle data + /// objects, each representing a single candle with open, high, low, close, and volume values. + pub fn get( + &self, + chain: C, + exchange: E, + pool: P, + options: CandleOptions, + ) -> Result + where + C: Into, + E: Into, + P: Into, + { + let mut parameters = HashMap::new(); + + // required + parameters.insert("chain".to_string(), chain.into()); + parameters.insert("exchange".to_string(), exchange.into()); + parameters.insert("pool".to_string(), pool.into()); + + // optional + parameters.extend( + [ + options + .market + .map(|market| ("market".to_string(), market.to_string())), + options + .interval + .map(|interval| ("interval".to_string(), interval.to_string())), + options + .page + .map(|page| ("page".to_string(), page.to_string())), + options + .limit + .map(|limit| ("limit".to_string(), limit.to_string())), + options + .from + .map(|from| ("from".to_string(), from.to_string())), + options.to.map(|to| ("to".to_string(), to.to_string())), + options + .sort + .map(|sort| ("sort".to_string(), sort.to_string())), + ] + .into_iter() + .flatten(), + ); + + self.client.get("/dex/candle", Some(parameters)) + } + + /// Retrieves information about available pools, including details about the chain, exchange, + /// base and quote symbols, and pool address. Optional parameters can be provided to filter the + /// results by chain and exchange. + pub fn pools(&self, options: PoolsOptions) -> Result> { + let mut parameters = HashMap::new(); + + // optional + parameters.extend( + [ + options + .exchange + .map(|exchange| ("exchange".to_string(), exchange.to_string())), + options + .chain + .map(|chain| ("chain".to_string(), chain.to_string())), + ] + .into_iter() + .flatten(), + ); + + self.client.get("/dex/candle/pools", Some(parameters)) + } + + /// Retrieves a list of available chains for candle data. + pub fn chains(&self) -> Result> { + self.client.get("/dex/candle/chains", None) + } + + /// Retrieves a list of available exchanges for candle data. + pub fn exchanges(&self) -> Result> { + self.client.get("/dex/candle/exchanges", None) + } + + /// Retrieves a list of available intervals for candle data. + pub fn intervals(&self) -> Result> { + self.client.get("/dex/candle/intervals", None) + } +} + +/// Implements the `Datamaxi` trait for `Candle`, providing methods +/// to create new instances of `Candle` with or without a custom base URL. +impl Datamaxi for Candle { + /// Creates a new `Candle` instance with the default base URL. + /// + /// # Parameters + /// - `api_key`: A `String` representing the API key used to authenticate requests. + /// + /// # Returns + /// A new `Candle` instance configured with the default base URL and the provided `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let candle = datamaxi::dex::Candle::new("my_api_key".to_string()); + /// ``` + fn new(api_key: String) -> Candle { + let config = Config { + base_url: None, // Default base URL will be used + api_key, // Provided API key + }; + Candle { + client: Client::new(config), // Create a new client with the provided config + } + } + + /// Creates a new `Candle` instance with a custom base URL. + /// + /// # Parameters + /// - `api_key`: A `String` representing the API key used to authenticate requests. + /// - `base_url`: A `String` representing the custom base URL for API requests. + /// + /// # Returns + /// A new `Candle` instance configured with the provided `base_url` and `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let candle = datamaxi::dex::Candle::new_with_base_url("my_api_key".to_string(), "https://custom-api.example.com".to_string()); + /// ``` + fn new_with_base_url(api_key: String, base_url: String) -> Candle { + let config = Config { + base_url: Some(base_url), // Use the provided custom base URL + api_key, // Provided API key + }; + Candle { + client: Client::new(config), // Create a new client with the provided config + } + } +} + +/// Provides methods for retrieving DEX trade data and related information. +#[derive(Clone)] +pub struct Trade { + pub client: Client, +} + +impl Trade { + /// Retrieves trade data for a specified chain, exchange, and pool. Additional parameters can be + /// provided to filter and sort the results. The response will contain an array of trade data + /// objects, each representing a single trade with price, amount, and timestamp values. + pub fn get( + &self, + chain: C, + exchange: E, + pool: P, + options: TradeOptions, + ) -> Result + where + C: Into, + E: Into, + P: Into, + { + let mut parameters = HashMap::new(); + + // required + parameters.insert("chain".to_string(), chain.into()); + parameters.insert("exchange".to_string(), exchange.into()); + parameters.insert("pool".to_string(), pool.into()); + + // optional + parameters.extend( + [ + options + .page + .map(|page| ("page".to_string(), page.to_string())), + options + .limit + .map(|limit| ("limit".to_string(), limit.to_string())), + options + .from + .map(|from| ("from".to_string(), from.to_string())), + options.to.map(|to| ("to".to_string(), to.to_string())), + options + .sort + .map(|sort| ("sort".to_string(), sort.to_string())), + ] + .into_iter() + .flatten(), + ); + + self.client.get("/dex/trade", Some(parameters)) + } + + /// Retrieves information about available pools, including details about the chain, exchange, + /// base and quote symbols, and pool address. Optional parameters can be provided to filter the + /// results by chain and exchange. + pub fn pools(&self, options: PoolsOptions) -> Result> { + let mut parameters = HashMap::new(); + + // optional + parameters.extend( + [ + options + .chain + .map(|chain| ("chain".to_string(), chain.to_string())), + options + .exchange + .map(|exchange| ("exchange".to_string(), exchange.to_string())), + ] + .into_iter() + .flatten(), + ); + + self.client.get("/dex/trade/pools", Some(parameters)) + } + + /// Retrieves a list of available intervals for trade data. + pub fn chains(&self) -> Result> { + self.client.get("/dex/trade/chains", None) + } + + /// Retrieves a list of available exchanges for trade data. + pub fn exchanges(&self) -> Result> { + self.client.get("/dex/trade/exchanges", None) + } +} + +/// Implements the `Datamaxi` trait for `Trade`, providing methods +/// to create new instances of `Trade` with or without a custom base URL. +impl Datamaxi for Trade { + /// Creates a new `Trade` instance with the default base URL. + /// + /// # Parameters + /// - `api_key`: A `String` containing the API key to authenticate requests. + /// + /// # Returns + /// A new `Trade` instance configured with the default base URL and the provided `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let trade = datamaxi::dex::Trade::new("my_api_key".to_string()); + /// ``` + fn new(api_key: String) -> Trade { + let config = Config { + base_url: None, // Use the default base URL + api_key, // Provided API key + }; + Trade { + client: Client::new(config), // Create a new client with the given config + } + } + + /// Creates a new `Trade` instance with a custom base URL. + /// + /// # Parameters + /// - `api_key`: A `String` containing the API key to authenticate requests. + /// - `base_url`: A `String` specifying the custom base URL for API requests. + /// + /// # Returns + /// A new `Trade` instance configured with the provided `base_url` and `api_key`. + /// + /// # Example + /// ```rust + /// use crate::datamaxi::api::Datamaxi; + /// let trade = datamaxi::dex::Trade::new_with_base_url("my_api_key".to_string(), "https://custom-api.example.com".to_string()); + /// ``` + fn new_with_base_url(api_key: String, base_url: String) -> Trade { + let config = Config { + base_url: Some(base_url), // Use the provided custom base URL + api_key, // Provided API key + }; + Trade { + client: Client::new(config), // Create a new client with the given config + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e5ae484 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,205 @@ +//! # DataMaxi+ Rust SDK +//! +//! This is the official implementation of Rust SDK for [DataMaxi+](https://datamaxiplus.com/). +//! The package can be used to fetch both historical and latest data using [DataMaxi+ API](https://docs.datamaxiplus.com/). +//! +//! - [Installation](#installation) +//! - [Configuration](#configuration) +//! - [Links](#links) +//! - [Contributing](#contributing) +//! - [License](#license) +//! +//! ## Installation +//! +//! ```shell +//! [dependencies] +//! datamaxi = { git = "https://github.com/bisonai/datamaxi-rust.git" } +//! ``` +//! +//! ## Configuration +//! +//! Private API endpoints are protected by an API key. +//! You can get the API key upon registering at . +//! +//! +//!| Option | Explanation | +//!|------------|-------------------------------------------------------------------------------| +//!| `api_key` | Your API key | +//!| `base_url` | If `base_url` is not provided, it defaults to `https://api.datamaxiplus.com`. | +//! +//! ## Examples +//! +//! ### CEX Candle +//! +//! ```rust +//! let api_key = "my_api_key".to_string(); +//! let candle: datamaxi::cex::Candle = datamaxi::api::Datamaxi::new(api_key); +//! +//! // Fetch supported exchanges for CEX candle data +//! candle.exchanges("spot"); +//! +//! // Fetch supported symbols for CEX candle data +//! let symbols_options = datamaxi::cex::SymbolsOptions::new(); +//! candle.symbols("binance", symbols_options); +//! +//! // Fetch supported intervals for CEX candle data +//! candle.intervals(); +//! +//! // Fetch CEX candle data +//! let candle_options = datamaxi::cex::CandleOptions::new(); +//! candle.get("binance", "ETH-USDT", candle_options); +//! ``` +//! +//! ### DEX Candle +//! +//! ```rust +//! let api_key = "my_api_key".to_string(); +//! let candle: datamaxi::dex::Candle = datamaxi::api::Datamaxi::new(api_key); +//! +//! // Fetch supported intervals for DEX candle data +//! candle.intervals(); +//! +//! // Fetch supported exchange for DEX candle data +//! candle.exchanges(); +//! +//! // Fetch supported chains for DEX candle data +//! candle.chains(); +//! +//! // Fetch supported pools for DEX candle data +//! let pools_options = datamaxi::dex::PoolsOptions::new(); +//! candle.pools(pools_options); +//! +//! // Fetch DEX candle data +//! let params = datamaxi::dex::CandleOptions::new(); +//! candle.get( +//! "bsc_mainnet", +//! "pancakeswap", +//! "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", +//! params, +//! ); +//! ``` +//! +//! ### DEX Trade +//! +//! ```rust +//! let api_key = "my_api_key".to_string(); +//! let trade: datamaxi::dex::Trade = datamaxi::api::Datamaxi::new(api_key); +//! +//! // Fetch supported exchange for DEX trade data +//! trade.exchanges(); +//! +//! // Fetch supported chains for DEX trade data +//! trade.chains(); +//! +//! // Fetch supported pools for DEX trade data +//! let pools_options = datamaxi::dex::PoolsOptions::new(); +//! trade.pools(pools_options); +//! +//! // Fetch DEX candle data +//! let trade_options = datamaxi::dex::TradeOptions::new().limit(5); +//! trade.get( +//! "bsc_mainnet", +//! "pancakeswap", +//! "0xb24cd29e32FaCDDf9e73831d5cD1FFcd1e535423", +//! trade_options +//! ); +//! ``` +//! +//! ## Links +//! +//! - [Official Website](https://datamaxiplus.com/) +//! - [Documentation](https://docs.datamaxiplus.com/) +//! +//! ## Contributing +//! +//! We welcome contributions! +//! If you discover a bug in this project, please feel free to open an issue to discuss the changes you would like to propose. +//! +//! ## License +//! +//![MIT License](./LICENSE) + +/// API definitions and related utilities. +pub mod api; + +/// CEX-related data fetcher and data structures. +/// +/// This module provides functionality related to centralized exchange (CEX) data. +/// It includes data structures and methods for retrieving candle data, as well as information about supported exchanges, symbols and intervals. +/// +/// # Usage +/// +/// The `Candle` struct is the primary interface for interacting with the CEX data. +/// It provides methods for retrieving data with optional parameters to filter and sort the results. +/// +/// ## Example +/// +/// ```rust +/// let config = datamaxi::api::Config { +/// base_url: None, +/// api_key: "my_api_key".to_string(), +/// }; +/// let client = datamaxi::api::Client::new(config); +/// let candle = datamaxi::cex::Candle { client: client.clone() }; +/// +/// // Retrieve supported exchanges +/// let exchanges = candle.exchanges("spot"); +/// +/// // Retrieve supported intervals +/// let intervals = candle.intervals(); +/// +/// // Retrieve supported Binance symbols +/// let symbols_options = datamaxi::cex::SymbolsOptions::new(); +/// let symbols = candle.symbols("binance", symbols_options); +/// +/// // Retrieve candle data +/// let candle_options = datamaxi::cex::CandleOptions::new().interval("1h").market("spot"); +/// let candle_data = candle.get("binance", "BTC-USDT", candle_options); +/// ``` +/// +/// # Error Handling +/// +/// All methods return a `Result` type, which should be handled appropriately to manage potential errors. +pub mod cex; + +/// DEX-related data fetcher and data structures. +/// +/// This module provides functionality related to decentralized exchange (DEX) data, +/// It includes data structures and methods for retrieving candle and trade data, as well as information supported chains, exchanges, pools and intervals. +/// +/// # Usage +/// +/// The `Candle` and `Trade` structs are the primary interfaces for interacting with the DEX data. +/// They provide methods for retrieving data with optional parameters to filter and sort the results. +/// +/// ## Example +/// +/// ```rust +/// let config = datamaxi::api::Config { +/// base_url: None, +/// api_key: "my_api_key".to_string(), +/// }; +/// let client = datamaxi::api::Client::new(config); +/// let candle = datamaxi::dex::Candle { client: client.clone() }; +/// let trade = datamaxi::dex::Trade { client }; +/// +/// // Retrieve candle data +/// let candle_options = datamaxi::dex::CandleOptions::new().interval("1h").limit(100); +/// let candle_data = candle.get("kaia_mainnet", "dragonswap", "0x...", candle_options); +/// +/// // Retrieve trade data +/// let trade_options = datamaxi::dex::TradeOptions::new().limit(50); +/// let trade_data = trade.get("kaia_mainnet", "dragonswap", "0x...", trade_options); +/// +/// // Retrieve available pools +/// let pools_options = datamaxi::dex::PoolsOptions::new().chain("kaia_mainnet"); +/// let pools = candle.pools(pools_options); +/// ``` +/// +/// # Error Handling +/// +/// All methods return a `Result` type, which should be handled appropriately to manage potential errors. +pub mod dex; + +/// Data models representing API responses and optional parameters. +pub mod models; diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..a23193a --- /dev/null +++ b/src/models.rs @@ -0,0 +1,401 @@ +use serde::Deserialize; +use serde::Serialize; + +/// Detailed information about a candle. +#[derive(Serialize, Deserialize, Debug)] +pub struct CandleDetail { + /// The timestamp of the candle's open time. + #[serde(rename = "d")] + pub timestamp: String, + + /// The opening price of the asset at the beginning of the time frame. + #[serde(rename = "o")] + pub open: String, + + /// The highest price of the asset during the time frame. + #[serde(rename = "h")] + pub high: String, + + /// The lowest price of the asset during the time frame. + #[serde(rename = "l")] + pub low: String, + + /// The closing price of the asset at the end of the time frame. + #[serde(rename = "c")] + pub close: String, + + /// The total volume of trades (in the base currency) that occurred during the time frame. + #[serde(rename = "v")] + pub volume: String, +} + +/// Response containing candle data. +#[derive(Serialize, Deserialize, Debug)] +pub struct CandleResponse { + /// A vector containing detailed information about each candle. + pub data: Vec, + + /// The current page number in the paginated response. + pub page: i32, + + /// The maximum number of items per page in the response. + pub limit: i32, + + /// The starting point of the time frame for the candle data. + pub from: String, + + /// The ending point of the time frame for the candle data. + pub to: String, + + /// The sorting order for the candle data (e.g., "asc" or "desc"). + pub sort: String, +} + +/// Detailed information about a trade. +#[derive(Serialize, Deserialize, Debug)] +pub struct TradeDetail { + /// The timestamp of the trade. + #[serde(rename = "d")] + pub timestamp: String, + + /// The block number in which the trade was recorded. + #[serde(rename = "b")] + pub block_number: i64, + + /// The trading pool where the trade occurred. + #[serde(rename = "pool")] + pub pool: String, + + /// The trading symbol associated with the trade (e.g., BTC-USDT). + #[serde(rename = "s")] + pub symbol: String, + + /// The hash of the transaction related to the trade. + #[serde(rename = "tx")] + pub transaction_hash: String, + + /// The maker of the trade (the party who placed the order). + #[serde(rename = "m")] + pub maker: String, + + /// The type of trade (e.g., buy or sell). + #[serde(rename = "t")] + pub trade_type: String, + + /// The quantity of the base asset traded, in base unit. + #[serde(rename = "bq")] + pub base_quantity: String, + + /// The quantity of the quote asset traded, in base unit. + #[serde(rename = "qq")] + pub quote_quantity: String, + + /// The price of the trade in the quote asset's unit. + #[serde(rename = "p")] + pub price: String, +} + +/// Response containing trade data. +#[derive(Serialize, Deserialize, Debug)] +pub struct TradeResponse { + /// A vector containing detailed information about each trade. + pub data: Vec, + + /// The current page number in the paginated response. + pub page: i32, + + /// The maximum number of items per page in the response. + pub limit: i32, + + /// The starting point of the time frame for the trade data. + pub from: String, + + /// The ending point of the time frame for the trade data. + pub to: String, + + /// The sorting order for the trade data (e.g., "asc" or "desc"). + pub sort: String, +} + +/// Optional parameters for a candle request. +pub struct CandleOptions { + /// The market type (e.g., spot, futures). + pub market: Option, + + /// The interval for the candle data (e.g., 1m, 1h, 1d). + pub interval: Option, + + /// The page number for the candle data. + pub page: Option, + + /// The maximum number of items per page in the response. + pub limit: Option, + + /// The starting date & time for the candle data. + pub from: Option, + + /// The ending date & time for the candle data. + pub to: Option, + + /// The sorting order for the candle data (e.g., "asc" or "desc"). + pub sort: Option, +} + +impl Default for CandleOptions { + fn default() -> Self { + Self::new() + } +} + +/// Provides a builder pattern for setting optional parameters for a candle request. +impl CandleOptions { + /// Creates a new instance of `CandleOptions` with default values. + pub fn new() -> Self { + CandleOptions { + market: None, + interval: None, + page: 1.into(), + limit: 1000.into(), + from: None, + to: None, + sort: Some("desc".into()), + } + } + + /// Sets the market for the candle query. + pub fn market(mut self, market: &str) -> Self { + self.market = Some(market.into()); + self + } + + /// Sets the interval for the candle query. + pub fn interval(mut self, interval: &str) -> Self { + self.interval = Some(interval.into()); + self + } + + /// Sets the page number for the candle query. + pub fn page(mut self, page: i32) -> Self { + self.page = Some(page); + self + } + + /// Sets the limit for the number of results returned. + pub fn limit(mut self, limit: i32) -> Self { + self.limit = Some(limit); + self + } + + /// Sets the starting date & time for the candle query. + pub fn from(mut self, from: &str) -> Self { + self.from = Some(from.into()); + self + } + + /// Sets the ending date & time for the candle query. + pub fn to(mut self, to: &str) -> Self { + self.to = Some(to.into()); + self + } + + /// Sets the sort order for the candle query (e.g., "asc" or "desc"). + pub fn sort(mut self, sort: &str) -> Self { + self.sort = Some(sort.into()); + self + } +} + +/// Response containing details about symbols. +#[derive(Serialize, Deserialize, Debug)] +pub struct SymbolsResponse { + /// The name of the exchange. + #[serde(rename = "e")] + pub exchange: String, + + /// The market type (e.g., spot, futures). + #[serde(rename = "m")] + pub market: String, + + /// The base asset of the trading pair. + #[serde(rename = "b")] + pub base: String, + + /// The quote asset of the trading pair. + #[serde(rename = "q")] + pub quote: String, + + /// The trading symbol (e.g., BTC-USDT). + #[serde(rename = "s")] + pub symbol: String, + + /// An optional unique identifier for the symbol. + #[serde(rename = "id")] + pub id: Option, +} + +/// Optional parameters for a symbols request. +pub struct SymbolsOptions { + /// The market type (e.g., spot, futures). + pub market: Option, +} + +impl Default for SymbolsOptions { + fn default() -> Self { + Self::new() + } +} + +impl SymbolsOptions { + /// Creates a new instance of `SymbolsOptions` with default values. + pub fn new() -> Self { + SymbolsOptions { market: None } + } + + /// Sets the market for the symbols query. + pub fn market(mut self, market: &str) -> Self { + self.market = Some(market.into()); + self + } +} + +/// Response containing details about pools. +#[derive(Serialize, Deserialize, Debug)] +pub struct PoolsResponse { + /// The blockchain where the pool is located. + #[serde(rename = "c")] + pub chain: String, + + /// The name of the exchange where the pool is available. + #[serde(rename = "e")] + pub exchange: String, + + /// The base asset used in the pool. + #[serde(rename = "b")] + pub base: String, + + /// The quote asset used in the pool. + #[serde(rename = "q")] + pub quote: String, + + /// The address of the base token in the pool. + #[serde(rename = "ba")] + pub baset_address: String, + + /// The address of the quote token in the pool. + #[serde(rename = "qa")] + pub quote_address: String, + + /// The unique address of the pool. + #[serde(rename = "pa")] + pub pool_address: String, + + /// An optional unique identifier for the pool. + #[serde(rename = "id")] + pub id: Option, +} + +/// Optional parameters for a trade request. +pub struct TradeOptions { + /// The page number for the trade query. + pub page: Option, + + /// The maximum number of items per page in the response. + pub limit: Option, + + /// The starting date for the trade query. + pub from: Option, + + /// The ending date for the trade query. + pub to: Option, + + /// The sorting order for the trade query (e.g., "asc" or "desc"). + pub sort: Option, +} + +impl Default for TradeOptions { + fn default() -> Self { + Self::new() + } +} + +/// Provides a builder pattern for setting optional parameters for a trade. +impl TradeOptions { + /// Creates a new instance of `TradeOptions` with default values. + pub fn new() -> Self { + TradeOptions { + page: 1.into(), + limit: 1000.into(), + from: None, + to: None, + sort: Some("desc".into()), + } + } + + /// Sets the page number for the trade query. + pub fn page(mut self, page: i32) -> Self { + self.page = Some(page); + self + } + + /// Sets the limit for the number of results returned. + pub fn limit(mut self, limit: i32) -> Self { + self.limit = Some(limit); + self + } + + /// Sets the starting date for the trade query. + pub fn from(mut self, from: &str) -> Self { + self.from = Some(from.into()); + self + } + + /// Sets the ending date for the trade query. + pub fn to(mut self, to: &str) -> Self { + self.to = Some(to.into()); + self + } + + /// Sets the sort order for the trade query (e.g., "asc" or "desc"). + pub fn sort(mut self, sort: &str) -> Self { + self.sort = Some(sort.into()); + self + } +} + +/// Optional parameters for a pools request. +pub struct PoolsOptions { + /// The chain for the pools query. + pub chain: Option, + + /// The exchange for the pools query. + pub exchange: Option, +} + +impl Default for PoolsOptions { + fn default() -> Self { + Self::new() + } +} + +/// Provides a builder pattern for setting optional parameters for a pools request. +impl PoolsOptions { + /// Creates a new instance of `PoolsOptions` with default values. + pub fn new() -> Self { + PoolsOptions { + chain: None, + exchange: None, + } + } + + /// Sets the chain for the pools query. + pub fn chain(mut self, chain: &str) -> Self { + self.chain = Some(chain.into()); + self + } + + /// Sets the exchange for the pools query. + pub fn exchange(mut self, exchange: &str) -> Self { + self.exchange = Some(exchange.into()); + self + } +}