riftenlabs-indexer/src/rpc/volume.rs
Dagur Valberg Johannsson 2c76bf09c7 Fix incorrect 5XX responses for user input errors
Return proper 4XX status codes for client errors to prevent load balancers
from misinterpreting input validation failures as server errors. Add
centralized ApiResult type and ApiErrorCode enum for consistent handling.
2026-01-21 08:27:57 +01:00

243 lines
7.9 KiB
Rust

// Copyright (C) 2025 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use crate::db::cauldron::pool::{get_token_volume_sats, get_total_volume_sats};
use crate::db::DB;
use crate::rpc::err::{db_error, ApiResult};
use crate::timeutil::time_now;
use rocket::get;
use rocket::serde::json::{json, Json, Value};
use rocket::State;
/// Gives total volume for all tokens in a time period.
/// Status: Stable
///
/// - start: Unix timestamp for start of period (optional, defaults to 24 hours ago)
/// - end: Unix timestamp for end of period (optional, defaults to now)
///
/// **Response Example:**
///
/// ```json
/// {
/// "total_volume_sats": 1459676788,
/// "period_start": 1640995200,
/// "period_end": 1641081600
/// }
/// ```
#[get("/volume?<start>&<end>")]
pub fn volume_all(start: Option<usize>, end: Option<usize>, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let end_timestamp = end.unwrap_or_else(|| time_now() as usize);
let start_timestamp = start.unwrap_or_else(|| {
end_timestamp.saturating_sub(86400) // 24 hours ago
});
let total_volume = get_total_volume_sats(&db, start_timestamp as u64, end_timestamp as u64)
.map_err(db_error)?;
Ok(Json(json!({
"total_volume_sats": total_volume,
"period_start": start_timestamp,
"period_end": end_timestamp
})))
}
/// Gives volume for a specific token in a time period.
/// Status: Stable
///
/// - token: Token identifier / category.
/// - start: Unix timestamp for start of period (optional, defaults to 24 hours ago)
/// - end: Unix timestamp for end of period (optional, defaults to now)
///
/// **Response Example:**
///
/// ```json
/// {
/// "volume_sats": 1459676788,
/// "volume_tokens": 12345,
/// "token_id": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
/// "period_start": 1640995200,
/// "period_end": 1641081600
/// }
/// ```
#[get("/volume/<token>?<start>&<end>")]
pub fn volume_token(
token: &str,
start: Option<usize>,
end: Option<usize>,
conn: &State<DB>,
) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let end_timestamp = end.unwrap_or_else(|| time_now() as usize);
let start_timestamp = start.unwrap_or_else(|| {
end_timestamp.saturating_sub(86400) // 24 hours ago
});
let (sats_volume, token_volume) =
get_token_volume_sats(&db, start_timestamp as u64, end_timestamp as u64, token)
.map_err(db_error)?;
Ok(Json(json!({
"volume_sats": sats_volume,
"volume_tokens": token_volume,
"token_id": token,
"period_start": start_timestamp,
"period_end": end_timestamp
})))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::cauldron::pool::{
create_table, dummy_init_seq, insert_new_pool, insert_pool_history_entry,
};
use crate::db::cauldron::tx::{create_table as create_tx_table, insert_block_tx};
use crate::db::cauldron::utxo_funding::create_table as create_utxo_funding_table;
use crate::utiltest::mock_db_pool;
use bitcoin_hashes::hex::ToHex;
use bitcoin_hashes::Hash;
use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid};
use riftenlabs_defi::cauldron::ParsedContract;
use riftenlabs_defi::chainutil::OutPointHash;
use rusqlite::Connection;
fn setup_mock_db(conn: &Connection) {
create_tx_table(conn);
create_table(conn);
create_utxo_funding_table(conn);
dummy_init_seq();
}
#[test]
fn test_volume_all() {
let mock_db = mock_db_pool(setup_mock_db);
let write_conn = mock_db.cauldron_w.get().unwrap();
// Insert test data
let owner_pkh =
PubkeyHash::from_hash(bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap());
let block_zero =
BlockHash::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap());
let current_time = time_now() as u64;
// Create test pool and history entries
let utxo =
OutPointHash::from_hash(bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap());
let token_id =
TokenID::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
let txid = Txid::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
let cauldron = ParsedContract {
pkh: owner_pkh,
is_withdrawn: false,
spent_utxo_hash: OutPointHash::from_hash(
bitcoin_hashes::sha256::Hash::from_slice(&[0x00; 32]).unwrap(),
),
new_utxo_hash: Some(utxo),
new_utxo_txid: Some(txid),
new_utxo_n: Some(0),
token_id: Some(token_id),
sats: Some(1000),
token_amount: Some(100),
};
insert_new_pool(&write_conn, &cauldron).unwrap();
insert_block_tx(
&write_conn,
&txid,
&block_zero,
current_time.try_into().unwrap(),
)
.unwrap();
// Insert pool history entry with volume
insert_pool_history_entry(
&write_conn,
&utxo,
&cauldron,
Some(current_time),
Some(current_time),
500, // sats_delta for trading activity
50, // token_delta for trading activity
)
.unwrap();
// Test volume calculation
let result =
get_total_volume_sats(&write_conn, current_time - 3600, current_time + 3600).unwrap();
assert_eq!(result, 500);
}
#[test]
fn test_volume_token() {
let mock_db = mock_db_pool(setup_mock_db);
let write_conn = mock_db.cauldron_w.get().unwrap();
// Insert test data
let owner_pkh =
PubkeyHash::from_hash(bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap());
let block_zero =
BlockHash::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap());
let current_time = time_now() as u64;
// Create test pool and history entries
let utxo =
OutPointHash::from_hash(bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap());
let token_id =
TokenID::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
let txid = Txid::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
let cauldron = ParsedContract {
pkh: owner_pkh,
is_withdrawn: false,
spent_utxo_hash: OutPointHash::from_hash(
bitcoin_hashes::sha256::Hash::from_slice(&[0x00; 32]).unwrap(),
),
new_utxo_hash: Some(utxo),
new_utxo_txid: Some(txid),
new_utxo_n: Some(0),
token_id: Some(token_id),
sats: Some(1000),
token_amount: Some(100),
};
insert_new_pool(&write_conn, &cauldron).unwrap();
insert_block_tx(
&write_conn,
&txid,
&block_zero,
current_time.try_into().unwrap(),
)
.unwrap();
// Insert pool history entry with volume
insert_pool_history_entry(
&write_conn,
&utxo,
&cauldron,
Some(current_time),
Some(current_time),
500, // sats_delta for trading activity
50, // token_delta for trading activity
)
.unwrap();
// Test token-specific volume calculation
let (sats_volume, token_volume) = get_token_volume_sats(
&write_conn,
current_time - 3600,
current_time + 3600,
&token_id.to_hex(),
)
.unwrap();
assert_eq!(sats_volume, 500);
assert_eq!(token_volume, 50);
}
}