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, } lazy_static! { static ref BSV_BI: RwLock = 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 = 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 = 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 = 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 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, blockchain: BlockchainSupported, ) -> BlockHeight { let bi: BlockchainInfo = blockchain.into(); time_to_blockheight(time, bi) } pub fn time_to_blockheight(time: DateTime, 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::>().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); } }