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.
243 lines
7.9 KiB
Rust
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);
|
|
}
|
|
}
|