Skip to content

Commit cef571e

Browse files
feat(aggregation-mode): add endpoint for submitted proofs count and time left (#2202)
Co-authored-by: Marcos Nicolau <[email protected]>
1 parent 844f778 commit cef571e

File tree

3 files changed

+96
-1
lines changed

3 files changed

+96
-1
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@ agg_mode_gateway_send_sp1_proof:
341341
--vk scripts/test_files/sp1/sp1_fibonacci_5_0_0_vk.bin \
342342
--private-key "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
343343

344+
agg_mode_get_quotas:
345+
curl -X GET http://127.0.0.1:8089/quotas/0x70997970C51812dc3A010C7d01b50e0d17dc79C8
346+
344347
__AGGREGATOR__: ## ____
345348

346349
aggregator_start: ## Start the Aggregator. Parameters: ENVIRONMENT=<devnet|testnet|mainnet>, AGG_CONFIG_FILE

aggregation_mode/gateway/src/helpers.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::{SystemTime, UNIX_EPOCH};
2+
13
pub(super) fn format_merkle_path(bytes: &[u8]) -> Result<Vec<String>, String> {
24
if bytes.is_empty() {
35
return Ok(vec![]);
@@ -12,3 +14,17 @@ pub(super) fn format_merkle_path(bytes: &[u8]) -> Result<Vec<String>, String> {
1214
.map(|chunk| format!("0x{}", hex::encode(chunk)))
1315
.collect())
1416
}
17+
18+
pub(crate) fn get_time_left_day_formatted() -> String {
19+
let now = SystemTime::now()
20+
.duration_since(UNIX_EPOCH)
21+
.expect("Error al obtener el tiempo");
22+
23+
let seconds_remaining = 86400 - (now.as_secs() % 86400);
24+
25+
let hours = seconds_remaining / 3600;
26+
let minutes = (seconds_remaining % 3600) / 60;
27+
let seconds = seconds_remaining % 60;
28+
29+
format!("{hours}:{minutes}:{seconds} UTC")
30+
}

aggregation_mode/gateway/src/http.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use super::{
2222
use crate::{
2323
config::Config,
2424
db::Db,
25+
helpers::get_time_left_day_formatted,
2526
types::{GetReceiptsResponse, SubmitProofRequestRisc0, SubmitProofRequestSP1},
2627
verifiers::{verify_sp1_proof, VerificationError},
2728
};
@@ -56,6 +57,7 @@ impl GatewayServer {
5657
.route("/receipts", web::get().to(Self::get_receipts))
5758
.route("/proof/sp1", web::post().to(Self::post_proof_sp1))
5859
.route("/proof/risc0", web::post().to(Self::post_proof_risc0))
60+
.route("/quotas/{address}", web::get().to(Self::get_quotas))
5961
})
6062
.bind(("127.0.0.1", port))
6163
.expect("To bind socket correctly")
@@ -147,8 +149,13 @@ impl GatewayServer {
147149
};
148150

149151
if daily_tasks_by_address >= state.config.max_daily_proofs_per_user {
152+
let formatted_time_left = get_time_left_day_formatted();
153+
150154
return HttpResponse::InternalServerError().json(AppResponse::new_unsucessfull(
151-
"Request denied: Query limit exceeded.",
155+
format!(
156+
"Request denied: Query limit exceeded. Quotas renew in {formatted_time_left}"
157+
)
158+
.as_str(),
152159
400,
153160
));
154161
}
@@ -322,4 +329,73 @@ impl GatewayServer {
322329
.json(AppResponse::new_unsucessfull("Internal server error", 500)),
323330
}
324331
}
332+
333+
async fn get_quotas(req: HttpRequest) -> impl Responder {
334+
let Some(state) = req.app_data::<Data<GatewayServer>>() else {
335+
return HttpResponse::InternalServerError().json(AppResponse::new_unsucessfull(
336+
"Internal server error: Failed to get app data",
337+
500,
338+
));
339+
};
340+
341+
let state = state.get_ref();
342+
343+
let Some(address_raw) = req.match_info().get("address") else {
344+
return HttpResponse::BadRequest()
345+
.json(AppResponse::new_unsucessfull("Missing address", 400));
346+
};
347+
348+
// Check that the address is a valid ethereum address
349+
if alloy::primitives::Address::from_str(address_raw.trim()).is_err() {
350+
return HttpResponse::BadRequest()
351+
.json(AppResponse::new_unsucessfull("Invalid address", 400));
352+
}
353+
354+
let address = address_raw.trim().to_lowercase();
355+
356+
let Ok(daily_tasks_by_address) = state.db.get_daily_tasks_by_address(&address).await else {
357+
return HttpResponse::InternalServerError()
358+
.json(AppResponse::new_unsucessfull("Internal server error", 500));
359+
};
360+
361+
let formatted_time_left = get_time_left_day_formatted();
362+
363+
let now_epoch = match SystemTime::now().duration_since(UNIX_EPOCH) {
364+
Ok(duration) => duration.as_secs(),
365+
Err(_) => {
366+
return HttpResponse::InternalServerError()
367+
.json(AppResponse::new_unsucessfull("Internal server error", 500));
368+
}
369+
};
370+
371+
let has_payment = match state
372+
.db
373+
.has_active_payment_event(
374+
&address,
375+
// safe unwrap the number comes from a valid u64 primitive
376+
BigDecimal::from_str(&now_epoch.to_string()).unwrap(),
377+
)
378+
.await
379+
{
380+
Ok(result) => result,
381+
Err(_) => {
382+
return HttpResponse::InternalServerError()
383+
.json(AppResponse::new_unsucessfull("Internal server error", 500));
384+
}
385+
};
386+
387+
if has_payment {
388+
HttpResponse::Ok().json(AppResponse::new_sucessfull(serde_json::json!({
389+
"proofs_submitted": daily_tasks_by_address,
390+
"quota_limit": state.config.max_daily_proofs_per_user,
391+
"quota_remaining": (state.config.max_daily_proofs_per_user - daily_tasks_by_address),
392+
"quota_resets_in": formatted_time_left.as_str()
393+
})))
394+
} else {
395+
HttpResponse::Ok().json(AppResponse::new_unsucessfull(
396+
"The address doesn't have an active subscription",
397+
404,
398+
))
399+
}
400+
}
325401
}

0 commit comments

Comments
 (0)