259 lines
8.4 KiB
Rust
259 lines
8.4 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::timeutil::time_now;
|
||
|
|
use rocket::get;
|
||
|
|
use rocket::http::Status;
|
||
|
|
use rocket::response::status::Custom;
|
||
|
|
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>,
|
||
|
|
) -> Result<Json<Value>, Custom<String>> {
|
||
|
|
let db = conn.cauldron_r.get().map_err(|_| {
|
||
|
|
Custom(
|
||
|
|
Status::InternalServerError,
|
||
|
|
"Failed to get DB connection".into(),
|
||
|
|
)
|
||
|
|
})?;
|
||
|
|
|
||
|
|
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(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
|
||
|
|
|
||
|
|
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>,
|
||
|
|
) -> Result<Json<Value>, Custom<String>> {
|
||
|
|
let db = conn.cauldron_r.get().map_err(|_| {
|
||
|
|
Custom(
|
||
|
|
Status::InternalServerError,
|
||
|
|
"Failed to get DB connection".into(),
|
||
|
|
)
|
||
|
|
})?;
|
||
|
|
|
||
|
|
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(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|