Skip to content

Commit 0da2dc3

Browse files
authored
Merge pull request #56 from Web3Novalabs/volume-analysis
Implement volume analysis endpoint for admin
2 parents 5919139 + 491d6b2 commit 0da2dc3

File tree

9 files changed

+203
-1
lines changed

9 files changed

+203
-1
lines changed

server/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ path = "src/main.rs"
1919
path = "src/lib.rs"
2020

2121
[dependencies]
22-
axum = "0.8.4"
22+
axum = { version = "0.8.4", features = ["macros"] }
2323
serde = {version="1.0.219", features=["derive"]}
2424
tokio = { version = "1.45.1", features = ["full"] }
2525
starknet = "0.17.0"

server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod libs {
1212

1313
pub mod routes {
1414
pub mod admin;
15+
pub mod analytics;
1516
pub mod crowd_funding;
1617
pub mod groups;
1718
pub mod health;

server/src/libs/router.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn router(state: AppState) -> Router {
3535
.nest("/groups", routes::groups::router())
3636
.nest("/admin", routes::admin::router())
3737
.nest("/crowdfunding", routes::crowd_funding::router())
38+
.nest("/analytics", routes::analytics::router())
3839
.with_state(state)
3940
.layer(cors)
4041
.fallback(|| async { (StatusCode::UNAUTHORIZED, "UNAUTHORIZED ORIGIN") })

server/src/routes/analytics.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use utoipa_axum::{router::OpenApiRouter, routes};
2+
3+
use crate::AppState;
4+
5+
pub mod analytics_admin_routes;
6+
pub mod analytics_type;
7+
pub mod analytics_user_routes;
8+
pub fn router() -> OpenApiRouter<AppState> {
9+
let admin_analytics = OpenApiRouter::new().routes(routes!(analytics_admin_routes::get_volume));
10+
11+
OpenApiRouter::new().nest("/admin", admin_analytics)
12+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use axum::{
2+
Json,
3+
extract::{Query, State},
4+
};
5+
use bigdecimal::BigDecimal;
6+
7+
use crate::{
8+
AppState,
9+
libs::{
10+
auth::AdminUser,
11+
error::{ApiError, map_sqlx_error},
12+
utopia::ADMIN_TAG,
13+
},
14+
routes::analytics::analytics_type::{
15+
AnalyticsItem, AnalyticsRow, VolumeDetails, VolumeRequest,
16+
}, util::validate_address::validate_date,
17+
};
18+
use sqlx::Arguments;
19+
#[axum::debug_handler]
20+
#[utoipa::path(
21+
method(get),
22+
description = "Get volume processed",
23+
path = "/volume",
24+
responses(
25+
(status = OK, description = "Success", body = VolumeDetails),
26+
(status = INTERNAL_SERVER_ERROR, description = "Database Error | Failed to get crowd funding", body = ApiError),
27+
(status = NOT_FOUND, description = "Crowd Funding Not Found", body = ApiError),
28+
(status = BAD_REQUEST, description = "Invalid Crowd Funding Address", body = ApiError),
29+
),
30+
params(
31+
VolumeRequest,
32+
),
33+
tag = ADMIN_TAG,
34+
security(
35+
("bearer" = [])
36+
)
37+
)]
38+
pub async fn get_volume(
39+
State(state): State<AppState>,
40+
AdminUser(_hello): AdminUser,
41+
Query(params): Query<VolumeRequest>,
42+
) -> Result<Json<Vec<AnalyticsItem>>, ApiError> {
43+
let mut base_sql_command = String::from("SELECT *
44+
FROM (
45+
SELECT token_address, token_amount, created_at::timestamptz AS time, 'group_tx_hashes' AS source
46+
FROM group_tx_hashes
47+
48+
UNION ALL
49+
50+
SELECT token_address, amount AS token_amount, paid_at::timestamptz AS time, 'payments' AS source
51+
FROM payments
52+
53+
UNION ALL
54+
55+
SELECT token_address, amount AS token_amount, created_at::timestamptz AS time, 'donations' AS source
56+
FROM donations
57+
58+
UNION ALL
59+
60+
SELECT token_address, amount AS token_amount, created_at::timestamptz AS time, 'withdrawals' AS source
61+
FROM withdrawals
62+
) AS all_tx");
63+
64+
let mut _i: i32 = 1;
65+
let mut args: sqlx::postgres::PgArguments = sqlx::postgres::PgArguments::default();
66+
67+
if let (Some(from), Some(to)) = (params.from.as_ref(), params.to.as_ref()) {
68+
validate_date(&from)?;
69+
validate_date(&to)?;
70+
base_sql_command.push_str(&format!(
71+
" WHERE time BETWEEN ${}::timestamptz AND ${}::timestamptz",
72+
_i,
73+
_i + 1
74+
));
75+
args.add(to.clone())
76+
.map_err(|_| ApiError::Internal("Failed to add limit arg"))?;
77+
args.add(from.clone())
78+
.map_err(|_| ApiError::Internal("Failed to add limit arg"))?;
79+
_i += 2;
80+
} else if let Some(from) = params.from.as_ref() {
81+
validate_date(&from)?;
82+
base_sql_command.push_str(&format!(" WHERE time >= ${}::timestamptz", _i));
83+
args.add(from.clone())
84+
.map_err(|_| ApiError::Internal("Failed to add limit arg"))?;
85+
_i += 1;
86+
} else if let Some(to) = params.to.as_ref() {
87+
validate_date(&to)?;
88+
base_sql_command.push_str(&format!(" WHERE time <= ${}::timestamptz", _i));
89+
args.add(to.clone())
90+
.map_err(|_| ApiError::Internal("Failed to add limit arg"))?;
91+
_i += 1;
92+
}
93+
94+
if let Some(token) = params.token.as_ref() {
95+
if _i == 1 {
96+
base_sql_command.push_str(&format!(" WHERE token_address = ${}", _i));
97+
} else {
98+
base_sql_command.push_str(&format!(" AND token_address = ${}", _i));
99+
}
100+
args.add(token.clone())
101+
.map_err(|_| ApiError::Internal("Failed to add limit arg"))?;
102+
_i += 1;
103+
}
104+
base_sql_command.push_str(" ORDER BY time DESC;");
105+
106+
// println!("params: {:?}", &params);
107+
// println!("SQL COMMAND: {}", &base_sql_command);
108+
// println!("ARGS: {:?}", &args);
109+
110+
let rows: Vec<AnalyticsRow> = sqlx::query_as_with(&base_sql_command, args)
111+
.fetch_all(&state.db)
112+
.await
113+
.map_err(|e| {
114+
dbg!(&e);
115+
map_sqlx_error(&e)
116+
})?;
117+
118+
let items: Vec<AnalyticsItem> = rows
119+
.into_iter()
120+
.map(
121+
|(token_address, token_amount, time, source)| AnalyticsItem {
122+
token_address: token_address,
123+
token_amount: BigDecimal::to_string(&token_amount),
124+
time: time.to_rfc3339(),
125+
source: source,
126+
},
127+
)
128+
.collect();
129+
130+
Ok(Json(items))
131+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use bigdecimal::BigDecimal;
2+
use serde::{Deserialize, Serialize};
3+
use sqlx::types::chrono;
4+
use utoipa::{IntoParams, ToSchema};
5+
6+
#[derive(Debug, Serialize, ToSchema)]
7+
pub struct VolumeDetails {}
8+
9+
#[derive(Debug, Serialize, ToSchema)]
10+
pub struct VolumeRecord {}
11+
12+
#[derive(Debug, Serialize, ToSchema)]
13+
pub struct VolumeParams {
14+
token: String,
15+
perios: String,
16+
total_inflow: String,
17+
total_outflow: String,
18+
net_volume: String,
19+
}
20+
21+
#[derive(Debug, Serialize, ToSchema, Deserialize, IntoParams)]
22+
pub struct VolumeRequest {
23+
pub from: Option<String>,
24+
pub to: Option<String>,
25+
pub token: Option<String>,
26+
}
27+
28+
pub type AnalyticsRow = (String, BigDecimal, chrono::DateTime<chrono::Utc>, String);
29+
30+
#[derive(Debug, Serialize, ToSchema)]
31+
pub struct AnalyticsItem {
32+
pub token_address: String,
33+
pub token_amount: String,
34+
pub time: String,
35+
pub source: String,
36+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

server/src/util/validate_address.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use sqlx::types::chrono;
12
use validator::ValidationError;
23

34
use crate::libs::error::ApiError;
@@ -17,3 +18,10 @@ pub fn validate_address_api_err(address: &str) -> Result<(), ApiError> {
1718
.then_some(())
1819
.ok_or(ApiError::BadRequest("INVALID ADDRESS FORMAT"))
1920
}
21+
22+
pub fn validate_date(date_str: &str) -> Result<(), ApiError> {
23+
match chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
24+
Ok(_) => Ok(()),
25+
Err(_) => Err(ApiError::BadRequest("INVALID DATE FORMAT")),
26+
}
27+
}

0 commit comments

Comments
 (0)