diff --git a/src/http/activity_log_retrieval.rs b/src/http/activity_log_retrieval.rs new file mode 100644 index 0000000..e0af1a2 --- /dev/null +++ b/src/http/activity_log_retrieval.rs @@ -0,0 +1,89 @@ +use crate::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use serde_json::{json, Value}; + +use super::types::{ActivityLogData, ActivityLogGetRequest, ActivityLogGetResponse}; +use crate::api_error::ApiError; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +pub async fn log_retrieval( + State(app_state): State, + Query(query_params): Query, +) -> Result, ApiError> { + // println!("\nLog Retrieval: {:?}\n", query_params); + + // Add default date if no cursor is provided + let cursor: String = match query_params.cursor { + Some(cursor1) => match OffsetDateTime::parse(&cursor1, &Rfc3339) { + Ok(cur) => cur.format(&Rfc3339).unwrap(), + Err(_) => return Err(ApiError::InvalidRequest("Invalid cursor".to_string())), + }, + + None => { + let now = OffsetDateTime::now_utc(); + now.format(&Rfc3339).unwrap() + } + }; + + let limit = match query_params.limit { + Some(l) => { + if !(1..=100).contains(&l) { + return Err(ApiError::InvalidRequest( + "Limit must be a number between 1 and 100".to_string(), + )); + } + l + } + None => 10, + }; + + println!("Limit: {}", limit); + let rows: Vec = sqlx::query_as::<_, ActivityLogData>( + r#" + SELECT + wallet_address, + from_token, + to_token, + amount_from, + percentage, + amount_to, + TO_CHAR(created_at, 'YYYY-MM-DD"T"HH24:MI:SSZ') AS created_at + FROM transactions_log + WHERE created_at < $1::TIMESTAMPTZ + ORDER BY created_at DESC + LIMIT $2 + "#, + ) + .bind(cursor) + .bind(limit) + .fetch_all(&app_state.db.pool) + .await + .map_err(ApiError::DatabaseError)?; + + // Map results to the response data structure + let mut response_data: ActivityLogGetResponse = ActivityLogGetResponse { + transactions: rows + .into_iter() + .map(|row| ActivityLogData { + wallet_address: row.wallet_address, + from_token: row.from_token, + to_token: row.to_token, + percentage: row.percentage, + amount_from: row.amount_from, + amount_to: row.amount_to, + created_at: row.created_at, + }) + .collect(), + next_cursor: None, + }; + + // Check if there are more transactions + if response_data.transactions.len() == limit as usize { + let last_transaction = response_data.transactions.last().unwrap(); + response_data.next_cursor = Some(last_transaction.created_at.clone()); + } + + Ok(Json(json!(response_data))) +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 3e54065..24d979c 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,11 +2,11 @@ use axum::{ routing::{get, post}, Router, }; +mod activity_log_retrieval; mod health_check; mod subscription; mod types; mod unsubscription; - use crate::AppState; // Application router. @@ -16,4 +16,5 @@ pub fn router() -> Router { .route("/health_check", get(health_check::health_check)) .route("/unsubscribe", post(unsubscription::handle_unsubscribe)) .route("/subscriptions", post(subscription::create_subscription)) + .route("/log_retrieval", get(activity_log_retrieval::log_retrieval)) } diff --git a/src/http/types.rs b/src/http/types.rs index d074a08..97b465f 100644 --- a/src/http/types.rs +++ b/src/http/types.rs @@ -1,9 +1,33 @@ use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use sqlx::FromRow; use std::fmt::Formatter; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +#[derive(Debug, Deserialize)] +pub struct ActivityLogGetRequest { + pub cursor: Option, + pub limit: Option, +} + +#[derive(FromRow, Debug, Serialize)] +pub struct ActivityLogData { + pub wallet_address: String, + pub from_token: String, + pub to_token: String, + pub percentage: i16, + pub amount_from: i64, + pub amount_to: i64, + pub created_at: String, +} + +#[derive(Debug, Serialize)] +pub struct ActivityLogGetResponse { + pub transactions: Vec, + pub next_cursor: Option, +} + #[derive(Debug, Deserialize)] pub struct CreateSubscriptionRequest { pub wallet_address: String, diff --git a/tests/api/activity_log_retrieval.rs b/tests/api/activity_log_retrieval.rs new file mode 100644 index 0000000..e424842 --- /dev/null +++ b/tests/api/activity_log_retrieval.rs @@ -0,0 +1,162 @@ +use axum::{ + body::{to_bytes, Body}, + http::{Request, StatusCode}, +}; +use serde::{Deserialize, Serialize}; + +use crate::helpers::*; +use sqlx::PgPool; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct ActivityLogGetResponse { + pub transactions: Vec, + pub next_cursor: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct ActivityLogData { + pub wallet_address: String, + pub from_token: String, + pub to_token: String, + pub percentage: i16, + pub amount_from: i64, + pub amount_to: i64, + pub created_at: String, +} + +async fn populate_db(pool: &PgPool) -> bool { + println!("----------------populating db"); + sqlx::query( + "INSERT INTO transactions_log ( + wallet_address, + from_token, + to_token, + percentage, + amount_from, + amount_to, + created_at, + updated_at + ) VALUES + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:42.728841+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:42.316783+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:41.917281+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:41.514413+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:41.08329+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:40.562681+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:40.053961+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:39.507289+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:38.464406+00', NULL), + ('0x1234567890abcdef8234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 70, 1870000000, 500600000, '2024-11-29 10:49:36.202316+00', NULL), + ('0x1234567890abcdef1234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 50, 100000000, 50000000, '2024-11-28 12:02:49.898622+00', NULL), + ('0x1234567890abcdef1234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 50, 100000000, 50000000, '2024-11-28 12:02:47.453754+00', NULL), + ('0x1234567890abcdef1234567890abcdef12345678', '0x9876543210fedcba9876543210fedcba98765432', '0x1111111111111111111111111111111111111111', 50, 100000000, 50000000, '2024-11-28 12:02:42.457038+00', NULL); + + ",).execute(pool).await.unwrap(); + true +} + +#[tokio::test] +async fn test_log_retrieval_pagination() { + let app = TestApp::new().await; + + sqlx::query!("DELETE FROM transactions_log") + .execute(&app.db.pool) + .await + .unwrap(); + + let req = Request::get("/log_retrieval?cursor=2024-11-30T10:49:36.20Z&limit=10") + .body(Body::empty()) + .unwrap(); + let resp = app.request(req).await; + + assert_eq!(resp.status(), StatusCode::OK); + + let body_bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let response_body: ActivityLogGetResponse = serde_json::from_slice(&body_bytes).unwrap(); + // println!("1: ///////////////////{:#?}", response_body); + + assert_eq!( + response_body.transactions.len(), + 0, + "Expected no transactions" + ); + + let _t = populate_db(&app.db.pool).await; + + let req = Request::get("/log_retrieval?cursor=2024-11-30T10:49:36Z&limit=10") + .body(Body::empty()) + .unwrap(); + let resp = app.request(req).await; + + assert_eq!(resp.status(), StatusCode::OK); + + let body_bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let response_body: ActivityLogGetResponse = serde_json::from_slice(&body_bytes).unwrap(); + // println!("2: ///////////////////{:#?}", response_body); + + assert_eq!(response_body.transactions.len(), 10); + + let next_cursor = response_body.next_cursor.unwrap(); + + // println!("Next Cursor: {}", next_cursor); + + assert_eq!(next_cursor, "2024-11-29T10:49:36Z".to_string()); + let url = format!("/log_retrieval?cursor={}&limit=10", next_cursor); + + let req = Request::get(&url).body(Body::empty()).unwrap(); + + let resp = app.request(req).await; + + assert_eq!(resp.status(), StatusCode::OK); + + let body_bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let response_body: ActivityLogGetResponse = serde_json::from_slice(&body_bytes).unwrap(); + // println!("3:///////////////////{:#?}", response_body); + + assert_eq!(response_body.transactions.len(), 3); + + assert_eq!(response_body.next_cursor, None); +} + +#[tokio::test] +async fn test_log_retrieval_no_cursor() { + let app = TestApp::new().await; + + let req = Request::get("/log_retrieval?limit=10") + .body(Body::empty()) + .unwrap(); + let resp = app.request(req).await; + assert_eq!(resp.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_log_retrieval_no_cursor_no_limit() { + let app = TestApp::new().await; + + let req = Request::get("/log_retrieval").body(Body::empty()).unwrap(); + let resp = app.request(req).await; + + assert_eq!(resp.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_log_retrieval_invalid_cursor() { + let app = TestApp::new().await; + + let req = Request::get("/log_retrieval?cursor=invalid") + .body(Body::empty()) + .unwrap(); + let resp = app.request(req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_log_retrieval_invalid_limit() { + let app = TestApp::new().await; + + let req = Request::get("/log_retrieval?limit=invalid") + .body(Body::empty()) + .unwrap(); + let resp = app.request(req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} diff --git a/tests/api/main.rs b/tests/api/main.rs index aab42b9..2913f37 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -1,3 +1,4 @@ +mod activity_log_retrieval; mod health_check; mod helpers; mod subscription;