240 lines
9.1 KiB
Rust
240 lines
9.1 KiB
Rust
use chrono::prelude::*;
|
|
use lazy_static::lazy_static;
|
|
use std::convert::Into;
|
|
use std::sync::RwLock;
|
|
|
|
pub enum BlockchainSupported {
|
|
BTC,
|
|
BSV,
|
|
XMR,
|
|
ETH,
|
|
}
|
|
|
|
type BlockHeight = i64;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
|
pub struct BlockchainInfo {
|
|
block_period: u64, // In seconds
|
|
latest_block_height: BlockHeight,
|
|
latest_block_time: DateTime<Utc>,
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref BSV_BI: RwLock<BlockchainInfo> = RwLock::new(BlockchainInfo {
|
|
block_period: 10 * 60, // 10 minutes,
|
|
latest_block_height: 852792,
|
|
latest_block_time: Utc.with_ymd_and_hms(2024, 7, 11, 19, 53, 7).unwrap(),
|
|
});
|
|
static ref BTC_BI: RwLock<BlockchainInfo> = RwLock::new(BlockchainInfo {
|
|
block_period: 10 * 60, // 10 minutes,
|
|
latest_block_height: 851724,
|
|
latest_block_time: Utc.with_ymd_and_hms(2024, 7, 11, 19, 38, 6).unwrap(),
|
|
});
|
|
static ref XMR_BI: RwLock<BlockchainInfo> = RwLock::new(BlockchainInfo {
|
|
block_period: 2 * 60, // 2 minutes,
|
|
latest_block_height: 3190705,
|
|
latest_block_time: Utc.with_ymd_and_hms(2024, 7, 11, 19, 54, 11).unwrap(),
|
|
});
|
|
static ref ETH_BI: RwLock<BlockchainInfo> = RwLock::new(BlockchainInfo {
|
|
block_period: 12,
|
|
latest_block_height: 20285486,
|
|
latest_block_time: Utc.with_ymd_and_hms(2024, 7, 11, 19, 53, 59).unwrap(),
|
|
});
|
|
}
|
|
|
|
impl From<BlockchainSupported> for BlockchainInfo {
|
|
fn from(val: BlockchainSupported) -> Self {
|
|
match val {
|
|
BlockchainSupported::BTC => *BTC_BI.read().unwrap(),
|
|
BlockchainSupported::BSV => *BSV_BI.read().unwrap(),
|
|
BlockchainSupported::XMR => *XMR_BI.read().unwrap(),
|
|
BlockchainSupported::ETH => *ETH_BI.read().unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn time_to_blockheight_specific_chain(
|
|
time: DateTime<Utc>,
|
|
blockchain: BlockchainSupported,
|
|
) -> BlockHeight {
|
|
let bi: BlockchainInfo = blockchain.into();
|
|
time_to_blockheight(time, bi)
|
|
}
|
|
|
|
pub fn time_to_blockheight(time: DateTime<Utc>, bi: BlockchainInfo) -> BlockHeight {
|
|
let time_difference = (time - bi.latest_block_time).num_seconds();
|
|
let num_blocks = time_difference / bi.block_period as i64;
|
|
num_blocks + bi.latest_block_height
|
|
}
|
|
|
|
pub fn update_bsv_blockheight() {
|
|
let whatsonchain_url = "https://api.whatsonchain.com/v1/bsv/main/block/headers";
|
|
match ureq::get(whatsonchain_url).call() {
|
|
Ok(resp) => {
|
|
let json: serde_json::Value = resp.into_json().unwrap();
|
|
let height = json[0].get("height").unwrap().as_i64().unwrap();
|
|
|
|
// I am presuming that "time" is when WhatsOnChain saw the block rather than the
|
|
// timestamp in the block. I could be very wrong about that though... As I understand
|
|
// the timestamp in the block has no garuntee of anything other than maybe it's more
|
|
// than the last one.
|
|
let unixtime = json[0].get("time").unwrap().as_i64().unwrap();
|
|
let datetime = Utc.timestamp_opt(unixtime, 0).unwrap();
|
|
{
|
|
let mut bi = BSV_BI.write().unwrap();
|
|
bi.latest_block_height = height;
|
|
bi.latest_block_time = datetime;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
println!("Err: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_btc_blockheight() {
|
|
let blockcypher_url = "https://api.blockcypher.com/v1/btc/main";
|
|
match ureq::get(blockcypher_url).call() {
|
|
Ok(resp) => {
|
|
let json: serde_json::Value = resp.into_json().unwrap();
|
|
let height = json.get("height").unwrap().as_i64().unwrap();
|
|
let time = json.get("time").unwrap().as_str().unwrap();
|
|
let datetime = time.parse::<DateTime<Utc>>().unwrap();
|
|
{
|
|
let mut bi = BTC_BI.write().unwrap();
|
|
bi.latest_block_height = height;
|
|
bi.latest_block_time = datetime;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
println!("Err: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// FIXME - use a different API. I think localmonero is going down soon
|
|
pub fn update_xmr_blockheight() {
|
|
let blockcypher_url = "https://localmonero.co/blocks/api/get_stats";
|
|
match ureq::get(blockcypher_url).call() {
|
|
Ok(resp) => {
|
|
let json: serde_json::Value = resp.into_json().unwrap();
|
|
let height = json.get("height").unwrap().as_i64().unwrap();
|
|
let unixtime = json.get("last_timestamp").unwrap().as_i64().unwrap();
|
|
let datetime = Utc.timestamp_opt(unixtime, 0).unwrap();
|
|
{
|
|
let mut bi = XMR_BI.write().unwrap();
|
|
bi.latest_block_height = height;
|
|
bi.latest_block_time = datetime;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
println!("Err: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use chrono::Duration;
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn get_latest_btc_blockinfo() {
|
|
let bi_a = *BTC_BI.read().unwrap();
|
|
update_btc_blockheight();
|
|
let bi_b = *BTC_BI.read().unwrap();
|
|
assert_ne!(bi_a, bi_b);
|
|
assert!(bi_a.latest_block_height < bi_b.latest_block_height);
|
|
assert!(bi_a.latest_block_time < bi_b.latest_block_time);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn get_latest_bsv_blockinfo() {
|
|
let bi_a = *BSV_BI.read().unwrap();
|
|
update_bsv_blockheight();
|
|
let bi_b = *BSV_BI.read().unwrap();
|
|
assert_ne!(bi_a, bi_b);
|
|
assert!(bi_a.latest_block_height < bi_b.latest_block_height);
|
|
assert!(bi_a.latest_block_time < bi_b.latest_block_time);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn get_latest_xmr_blockinfo() {
|
|
let bi_a = *XMR_BI.read().unwrap();
|
|
update_xmr_blockheight();
|
|
let bi_b = *XMR_BI.read().unwrap();
|
|
assert_ne!(bi_a, bi_b);
|
|
assert!(bi_a.latest_block_height < bi_b.latest_block_height);
|
|
assert!(bi_a.latest_block_time < bi_b.latest_block_time);
|
|
}
|
|
|
|
#[test]
|
|
fn one_block_time_away() {
|
|
let latest_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 10, 0).unwrap();
|
|
let wanted_future_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 20, 1).unwrap();
|
|
let blockchain_info = BlockchainInfo {
|
|
block_period: 60 * 10, // 10 minutes
|
|
latest_block_height: 1000,
|
|
latest_block_time,
|
|
};
|
|
let result = time_to_blockheight(wanted_future_block_time, blockchain_info);
|
|
assert_eq!(result, 1001);
|
|
}
|
|
|
|
#[test]
|
|
fn one_block_time_previous() {
|
|
let latest_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 20, 1).unwrap();
|
|
let wanted_future_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 10, 0).unwrap();
|
|
let blockchain_info = BlockchainInfo {
|
|
block_period: 60 * 10, // 10 minutes
|
|
latest_block_height: 1000,
|
|
latest_block_time,
|
|
};
|
|
let result = time_to_blockheight(wanted_future_block_time, blockchain_info);
|
|
assert_eq!(result, 999);
|
|
}
|
|
|
|
#[test]
|
|
fn negative_block_time() {
|
|
let latest_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 30, 0).unwrap();
|
|
let wanted_negative_block_time = Utc.with_ymd_and_hms(2024, 10, 10, 10, 10, 0).unwrap();
|
|
let blockchain_info = BlockchainInfo {
|
|
block_period: 60 * 10, // 10 minutes
|
|
latest_block_height: 1,
|
|
latest_block_time,
|
|
};
|
|
let result = time_to_blockheight(wanted_negative_block_time, blockchain_info);
|
|
assert_eq!(result, -1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_next_block_on_some_blockchains() {
|
|
// Doesn't feel like an overly good test - but I can't think of anyother way that's "dynamic"
|
|
// to me manually updating the times above
|
|
let bi: BlockchainInfo = BlockchainSupported::BTC.into();
|
|
let wanted_time = bi.latest_block_time + Duration::seconds(bi.block_period as i64 + 1);
|
|
let bh = time_to_blockheight_specific_chain(wanted_time, BlockchainSupported::BTC);
|
|
assert_eq!(bh, bi.latest_block_height + 1);
|
|
let bi: BlockchainInfo = BlockchainSupported::BSV.into();
|
|
let wanted_time = bi.latest_block_time + Duration::seconds(bi.block_period as i64 + 2);
|
|
let bh = time_to_blockheight_specific_chain(wanted_time, BlockchainSupported::BSV);
|
|
assert_eq!(bh, bi.latest_block_height + 1);
|
|
let bi: BlockchainInfo = BlockchainSupported::XMR.into();
|
|
let wanted_time = bi.latest_block_time + Duration::seconds(bi.block_period as i64 + 3);
|
|
let bh = time_to_blockheight_specific_chain(wanted_time, BlockchainSupported::XMR);
|
|
assert_eq!(bh, bi.latest_block_height + 1);
|
|
let bi: BlockchainInfo = BlockchainSupported::ETH.into();
|
|
let wanted_time = bi.latest_block_time + Duration::seconds(bi.block_period as i64 + 4);
|
|
let bh = time_to_blockheight_specific_chain(wanted_time, BlockchainSupported::ETH);
|
|
assert_eq!(bh, bi.latest_block_height + 1);
|
|
let bi: BlockchainInfo = BlockchainSupported::ETH.into();
|
|
let wanted_time = bi.latest_block_time + Duration::seconds(bi.block_period as i64 + 5);
|
|
let bh = time_to_blockheight_specific_chain(wanted_time, BlockchainSupported::XMR);
|
|
assert_ne!(bh, bi.latest_block_height + 1);
|
|
}
|
|
}
|