diff --git a/lib/Cargo.lock b/lib/Cargo.lock index eba2ebc6b5..d4fa4c6d06 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -214,7 +214,10 @@ version = "0.1.0" dependencies = [ "axum 0.7.1", "hyper 0.14.27", + "keccak-hash", + "log", "serde", + "thiserror", ] [[package]] @@ -423,6 +426,7 @@ checksum = "810a80b128d70e6ed2bdf3fe8ed72c0ae56f5f5948d01c2753282dd92a84fce8" dependencies = [ "async-trait", "axum-core 0.4.0", + "axum-macros", "bytes", "futures-util", "http 1.0.0", @@ -485,6 +489,18 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2edad600410b905404c594e2523549f1bcd4bded1e252c8f74524ccce0b867" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "backtrace" version = "0.3.69" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8a2f5d2687..aaf1ed6cd8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -76,7 +76,7 @@ jsonrpsee-core = "0.16" jsonrpsee-server = "0.16" jsonrpsee-types = "0.16" -axum = "0.7.1" +axum = { version = "0.7.1", features = ["macros"] } tempdir = "0.3" diff --git a/lib/ain-ocean/Cargo.toml b/lib/ain-ocean/Cargo.toml index 4ebf0ea35d..bc5204d243 100644 --- a/lib/ain-ocean/Cargo.toml +++ b/lib/ain-ocean/Cargo.toml @@ -7,5 +7,8 @@ edition = "2021" [dependencies] axum.workspace = true -serde.workspace = true hyper.workspace = true +keccak-hash.workspace = true +log.workspace = true +serde.workspace = true +thiserror.workspace = true diff --git a/lib/ain-ocean/src/api/block.rs b/lib/ain-ocean/src/api/block.rs index 19834e1eda..142d379dce 100644 --- a/lib/ain-ocean/src/api/block.rs +++ b/lib/ain-ocean/src/api/block.rs @@ -1,5 +1,7 @@ -use axum::{extract::Path, routing::get, Router}; -use serde::Deserialize; +use axum::{debug_handler, extract::{Path, Query}, Json, routing::get, Router}; +use serde::{Deserialize, Serialize}; + +use crate::api_paged_response::ApiPagedResponse; #[derive(Deserialize)] struct BlockId { @@ -11,8 +13,29 @@ struct BlockHash { hash: String, } -async fn list_blocks() -> String { - "List of blocks".to_string() +#[derive(Deserialize)] +pub struct ListBlocksQuery { + pub size: usize, + pub next: Option +} + +#[debug_handler] +async fn list_blocks(Query(query): Query) -> Json> { + // TODO(): query from db + // db::block::list(req).await... + let blocks = vec![ + Block { id: "0".into() }, + Block { id: "1".into() }, + Block { id: "2".into() }, + ]; + + Json(ApiPagedResponse + ::of( + blocks, + query.size, + |block| block.clone().id + ) + ) } async fn get_block(Path(BlockId { id }): Path) -> String { @@ -29,3 +52,33 @@ pub fn router() -> Router { .route("/:id", get(get_block)) .route("/:hash/transactions", get(get_transactions)) } + +#[derive(Clone, Debug, Serialize)] +#[serde(default)] +pub struct Block { + id: String, + // TODO(): type mapping + // hash: H256, + // previous_hash: H256, + + // height: u64, + // version: u64, + // time: u64, // ---------------| block time in seconds since epoch + // median_time: u64, // --------| meidan time of the past 11 block timestamps + + // transaction_count: u64, + + // difficulty: u64, + + // masternode: H256, + // minter: H256, + // minter_block_count: u64, + // reward: f64 + + // state_modifier: H256, + // merkle_root: H256, + + // size: u64, + // size_stripped: u64, + // weight: u64, +} diff --git a/lib/ain-ocean/src/api_paged_response.rs b/lib/ain-ocean/src/api_paged_response.rs new file mode 100644 index 0000000000..83ac978df9 --- /dev/null +++ b/lib/ain-ocean/src/api_paged_response.rs @@ -0,0 +1,150 @@ +use serde::Serialize; + +/// ApiPagedResponse indicates that this response of data array slice is part of a sorted list of items. +/// Items are part of a larger sorted list and the slice indicates a window within the large sorted list. +/// Each ApiPagedResponse holds the data array and the "token" for next part of the slice. +/// The next token should be passed via query 'next' and only used when getting the next slice. +/// Hence the first request, the next token is always empty and not provided. +/// +/// With ascending sorted list and a limit of 3 items per slice will have the behaviour as such. +/// +/// SORTED : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] | [10] +/// Query 1 : Data: [1] [2] [3], Next: 3, Operator: GT (>) +/// Query 2 : Data: [4] [5] [6], Next: 6, Operator: GT (>) +/// Query 3 : Data: [7] [8] [9], Next: 3, Operator: GT (>) +/// Query 4 : Data: [10], Next: undefined +/// +/// This design is resilient also mutating sorted list, where pagination is not. +/// +/// SORTED : [2] [4] [6] [8] [10] [12] [14] +/// Query 1 : Data: [2] [4] [6], Next: 6, Operator: GT (>) +/// +/// Being in a slice window, the larger sorted list can be mutated. +/// You only need the next token to get the next slice. +/// MUTATED : [2] [4] [7] [8] [9] [10] [12] [14] +/// Query 2 : Data: [7] [8] [9], Next: 6, Operator: GT (>) +/// +/// Limitations of this requires your dat astructure to always be sorted in one direction and your sort +/// indexes always fixed. Hence the moving down of the slice window, your operator will be greater than (GT). +/// While moving up your operator will be less than (GT). +/// +/// ASC : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] | +/// >3 >6 >9 +/// DESC : | [9] [8] [7] | [6] [5] [4] | [3] [2] [1] | +/// <7 <4 <1 +/// For developer quality life it's unwise to allow inclusive operator, it just creates more overhead +/// to understanding our services. No GTE or LTE, always GT and LE. Services must beclean and clear, +/// when the usage narrative is clear and so will the use of ease. LIST query must be dead simple. +/// Image travelling down the path, and getting a "next token" to get the next set of itmes to +/// continue walking. +/// +/// Because the limit is not part of the slice window your query mechanism should support varying size windows. +/// +/// DATA: | [1] [2] [3] | [4] [5] [6] [7] | [8] [9] | ... +/// | limit 3, >3 | limit 4, >7 | limit 2, >9 +/// For simplicity your API should not attempt to allow access to different sort indexes, be cognizant of +/// how our APIs are consumed. If we create a GET /blocks operation to list blocks what would the correct indexes +/// be 99% of the time? +/// +/// Answer: Blocks sorted by height in descending order, that's your sorted list and your slice window. +/// : <- Latest | [100] [99] [98] [97] [...] | Oldest -> +/// +#[derive(Debug, Serialize, PartialEq)] +pub struct ApiPagedResponse { + data: Vec, + page: ApiPage, +} + +#[derive(Debug, Serialize, PartialEq)] +struct ApiPage { + next: Option, +} + +impl ApiPagedResponse { + pub fn new(data: Vec, next: Option<&str>) -> Self { + Self { data, page: ApiPage{ next: next.map(Into::into) } } // Option<&str> -> Option + } + + pub fn next(data: Vec, next: Option<&str>) -> Self { + Self::new(data, next) + } + + pub fn of(data: Vec, limit: usize, next_provider: impl Fn(&T) -> String) -> Self { + if data.len() == limit && data.len() > 0 && limit > 0 { + let next = next_provider(&data[limit - 1]); + Self::next(data, Some(next.as_str())) + } else { + Self::next(data, None) + } + } + + pub fn empty() -> Self { + Self::new(Vec::new(), None) + } +} + +#[cfg(test)] +mod tests { + use super::{ApiPagedResponse, ApiPage}; + + #[derive(Clone, Debug)] + struct Item { + id: String, + sort: String, + } + + impl Item { + fn new(id: &str, sort: &str) -> Self { + Self { + id: id.into(), + sort: sort.into(), + } + } + } + + #[test] + fn should_next_with_none() { + let items: Vec = vec![ + Item::new("0", "a"), + Item::new("1", "b"), + ]; + + let next = ApiPagedResponse::next(items, None).page.next; + assert_eq!(next, None); + } + + #[test] + fn should_next_with_value() { + let items: Vec = vec![ + Item::new("0", "a"), + Item::new("1", "b"), + ]; + + let next = ApiPagedResponse::next(items, Some("b")).page.next; + assert_eq!(next, Some("b".into())); + } + + #[test] + fn should_of_with_limit_3() { + let items: Vec = vec![ + Item::new("0", "a"), + Item::new("1", "b"), + Item::new("2", "c"), + ]; + + let next = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page.next; + assert_eq!(next, Some("c".into())) + } + + #[test] + fn should_not_create_with_limit_3_while_size_2() { + let items: Vec = vec![ + Item::new("0", "a"), + Item::new("1", "b"), + ]; + + let page = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page; + assert_eq!(page, ApiPage{next: None}) + } + +} diff --git a/lib/ain-ocean/src/error.rs b/lib/ain-ocean/src/error.rs new file mode 100644 index 0000000000..7064679280 --- /dev/null +++ b/lib/ain-ocean/src/error.rs @@ -0,0 +1,19 @@ +use axum::{http::StatusCode, response::{IntoResponse, Response}}; +use thiserror::Error; + +pub type OceanResult = Result; + +#[derive(Error, Debug)] +pub enum OceanError { +} + +impl IntoResponse for OceanError { + fn into_response(self) -> Response { + let code: StatusCode = match self { + // OceanError::SomeError => StatusCode::SomeCode, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + let reason = self.to_string(); + (code, reason).into_response() + } +} diff --git a/lib/ain-ocean/src/lib.rs b/lib/ain-ocean/src/lib.rs index 2dae6056be..7ee6252f32 100644 --- a/lib/ain-ocean/src/lib.rs +++ b/lib/ain-ocean/src/lib.rs @@ -1,4 +1,6 @@ mod api; +pub mod api_paged_response; +pub mod error; mod model; pub use api::ocean_router;