Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
5 changes: 4 additions & 1 deletion lib/ain-ocean/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 57 additions & 4 deletions lib/ain-ocean/src/api/block.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<String>
}

#[debug_handler]
async fn list_blocks(Query(query): Query<ListBlocksQuery>) -> Json<ApiPagedResponse<Block>> {
// 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<BlockId>) -> String {
Expand All @@ -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,
}
150 changes: 150 additions & 0 deletions lib/ain-ocean/src/api_paged_response.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
data: Vec<T>,
page: ApiPage,
}

#[derive(Debug, Serialize, PartialEq)]
struct ApiPage {
next: Option<String>,
}

impl<T> ApiPagedResponse<T> {
pub fn new(data: Vec<T>, next: Option<&str>) -> Self {
Self { data, page: ApiPage{ next: next.map(Into::into) } } // Option<&str> -> Option<String>
}

pub fn next(data: Vec<T>, next: Option<&str>) -> Self {
Self::new(data, next)
}

pub fn of(data: Vec<T>, 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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})
}

}
19 changes: 19 additions & 0 deletions lib/ain-ocean/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use axum::{http::StatusCode, response::{IntoResponse, Response}};
use thiserror::Error;

pub type OceanResult<T> = Result<T, OceanError>;

#[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()
}
}
2 changes: 2 additions & 0 deletions lib/ain-ocean/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod api;
pub mod api_paged_response;
pub mod error;
mod model;

pub use api::ocean_router;