diff --git a/src/epoch.rs b/src/epoch.rs index e49d1b767d..c93223989c 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -99,6 +99,7 @@ mod tests { (Epoch(0).subsidy() + Epoch(1).subsidy()) * Epoch::BLOCKS ); assert_eq!(Epoch(33).starting_ordinal(), Ordinal(Ordinal::SUPPLY)); + assert_eq!(Epoch(34).starting_ordinal(), Ordinal(Ordinal::SUPPLY)); } #[test] diff --git a/src/height.rs b/src/height.rs index 2fbbbfc278..89cce0292c 100644 --- a/src/height.rs +++ b/src/height.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Copy, Clone, Debug, Display)] +#[derive(Copy, Clone, Debug, Display, FromStr)] pub(crate) struct Height(pub(crate) u64); impl Height { @@ -42,14 +42,6 @@ impl PartialEq for Height { } } -impl FromStr for Height { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(Self(s.parse::()?)) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index c7b0920463..a740004730 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use { arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index, nft::Nft, options::Options, ordinal::Ordinal, sat_point::SatPoint, subcommand::Subcommand, }, - anyhow::{anyhow, Context, Error}, + anyhow::{anyhow, bail, Context, Error}, axum::{extract, http::StatusCode, response::IntoResponse, routing::get, Json, Router}, axum_server::Handle, bdk::{ diff --git a/src/ordinal.rs b/src/ordinal.rs index a9379fb5b3..e7303cc403 100644 --- a/src/ordinal.rs +++ b/src/ordinal.rs @@ -1,8 +1,6 @@ use super::*; -#[derive( - Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, FromStr, Deserialize, Serialize, -)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, Deserialize, Serialize)] #[serde(transparent)] pub(crate) struct Ordinal(pub(crate) u64); @@ -27,7 +25,7 @@ impl Ordinal { } pub(crate) fn period(self) -> u64 { - self.0 / PERIOD_BLOCKS + self.height().n() / PERIOD_BLOCKS } pub(crate) fn subsidy_position(self) -> u64 { @@ -52,6 +50,72 @@ impl Ordinal { } name.chars().rev().collect() } + + fn from_degree(s: &str) -> Result { + let (cycle_number, rest) = s + .split_once('°') + .ok_or_else(|| anyhow!("Missing degree symbol"))?; + let cycle_number = cycle_number.parse::()?; + + let (epoch_offset, rest) = rest + .split_once('′') + .ok_or_else(|| anyhow!("Missing prime symbol"))?; + let epoch_offset = epoch_offset.parse::()?; + if epoch_offset >= Epoch::BLOCKS { + bail!("Invalid epoch offset"); + } + + let (period_offset, rest) = rest + .split_once('″') + .ok_or_else(|| anyhow!("Missing double prime symbol"))?; + let period_offset = period_offset.parse::()?; + if period_offset >= PERIOD_BLOCKS { + bail!("Invalid period offset"); + } + + let cycle_start_epoch = cycle_number * CYCLE_EPOCHS; + + let cycle_progression = period_offset + .checked_sub(epoch_offset % PERIOD_BLOCKS) + .ok_or_else(|| anyhow!("Invalid relationship between epoch offset and period offset"))?; + + if cycle_progression % (Epoch::BLOCKS % PERIOD_BLOCKS) != 0 { + bail!("Invalid relationship between epoch offset and period offset"); + } + + let epochs_since_cycle_start = cycle_progression / (Epoch::BLOCKS % PERIOD_BLOCKS); + + let epoch = cycle_start_epoch + epochs_since_cycle_start; + + let height = Height(epoch * Epoch::BLOCKS + epoch_offset); + + let (block_offset, rest) = match rest.split_once('‴') { + Some((block_offset, rest)) => (block_offset.parse::()?, rest), + None => (0, rest), + }; + + if !rest.is_empty() { + bail!("Trailing characters"); + } + + if block_offset >= height.subsidy() { + bail!("Invalid block offset"); + } + + Ok(height.starting_ordinal() + block_offset) + } + + fn from_decimal(s: &str) -> Result { + let (height, offset) = s.split_once('.').ok_or_else(|| anyhow!("Missing period"))?; + let height = Height(height.parse()?); + let offset = offset.parse::()?; + + if offset >= height.subsidy() { + bail!("Invalid block offset"); + } + + Ok(height.starting_ordinal() + offset) + } } impl PartialEq for Ordinal { @@ -74,6 +138,25 @@ impl AddAssign for Ordinal { } } +impl FromStr for Ordinal { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.contains('°') { + Self::from_degree(s) + } else if s.contains('.') { + Self::from_decimal(s) + } else { + let ordinal = Self(s.parse()?); + if ordinal > Self::LAST { + Err(anyhow!("Invalid ordinal")) + } else { + Ok(ordinal) + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -102,6 +185,13 @@ mod tests { assert_eq!(Ordinal(2099999997689999).name(), "a"); } + #[test] + fn period() { + assert_eq!(Ordinal(0).period(), 0); + assert_eq!(Ordinal(10080000000000).period(), 1); + assert_eq!(Ordinal(2099999997689999).period(), 3437); + } + #[test] fn epoch() { assert_eq!(Ordinal(0).epoch(), 0); @@ -177,10 +267,91 @@ mod tests { assert_eq!(ordinal, 101); } + fn parse(s: &str) -> Result { + s.parse::().map_err(|e| e.to_string()) + } + + #[test] + fn from_str_decimal() { + assert_eq!(parse("0.0").unwrap(), 0); + assert_eq!(parse("0.1").unwrap(), 1); + assert_eq!(parse("1.0").unwrap(), 50 * 100_000_000); + assert_eq!(parse("6929999.0").unwrap(), 2099999997689999); + assert!(parse("0.5000000000").is_err()); + assert!(parse("6930000.0").is_err()); + } + + #[test] + fn from_str_degree() { + assert_eq!(parse("0°0′0″0‴").unwrap(), 0); + assert_eq!(parse("0°0′0″").unwrap(), 0); + assert_eq!(parse("0°0′0″1‴").unwrap(), 1); + assert_eq!(parse("0°2015′2015″0‴").unwrap(), 10075000000000); + assert_eq!(parse("0°2016′0″0‴").unwrap(), 10080000000000); + assert_eq!(parse("0°2017′1″0‴").unwrap(), 10085000000000); + assert_eq!(parse("0°2016′0″1‴").unwrap(), 10080000000001); + assert_eq!(parse("0°2017′1″1‴").unwrap(), 10085000000001); + assert_eq!(parse("0°209999′335″0‴").unwrap(), 1049995000000000); + assert_eq!(parse("0°0′336″0‴").unwrap(), 1050000000000000); + assert_eq!(parse("0°0′672″0‴").unwrap(), 1575000000000000); + assert_eq!(parse("0°209999′1007″0‴").unwrap(), 1837498750000000); + assert_eq!(parse("0°0′1008″0‴").unwrap(), 1837500000000000); + assert_eq!(parse("1°0′0″0‴").unwrap(), 2067187500000000); + assert_eq!(parse("2°0′0″0‴").unwrap(), 2099487304530000); + assert_eq!(parse("3°0′0″0‴").unwrap(), 2099991988080000); + assert_eq!(parse("4°0′0″0‴").unwrap(), 2099999873370000); + assert_eq!(parse("5°0′0″0‴").unwrap(), 2099999996220000); + assert_eq!(parse("5°0′336″0‴").unwrap(), 2099999997060000); + assert_eq!(parse("5°0′672″0‴").unwrap(), 2099999997480000); + assert_eq!(parse("5°1′673″0‴").unwrap(), 2099999997480001); + assert_eq!(parse("5°209999′1007″0‴").unwrap(), 2099999997689999); + } + + #[test] + fn from_str_number() { + assert_eq!(parse("0").unwrap(), 0); + assert!(parse("foo").is_err()); + assert_eq!(parse("2099999997689999").unwrap(), 2099999997689999); + assert!(parse("2099999997690000").is_err()); + } + + #[test] + fn from_str_degree_invalid_cycle_number() { + assert!(parse("5°0′0″0‴").is_ok()); + assert!(parse("6°0′0″0‴").is_err()); + } + + #[test] + fn from_str_degree_invalid_epoch_offset() { + assert!(parse("0°209999′335″0‴").is_ok()); + assert!(parse("0°210000′336″0‴").is_err()); + } + + #[test] + fn from_str_degree_invalid_period_offset() { + assert!(parse("0°2015′2015″0‴").is_ok()); + assert!(parse("0°2016′2016″0‴").is_err()); + } + + #[test] + fn from_str_degree_invalid_block_offset() { + assert!(parse("0°0′0″4999999999‴").is_ok()); + assert!(parse("0°0′0″5000000000‴").is_err()); + assert!(parse("0°209999′335″4999999999‴").is_ok()); + assert!(parse("0°0′336″4999999999‴").is_err()); + } + + #[test] + fn from_str_degree_invalid_period_block_relationship() { + assert!(parse("0°2015′2015″0‴").is_ok()); + assert!(parse("0°2016′0″0‴").is_ok()); + assert!(parse("0°2016′1″0‴").is_err()); + } + #[test] - fn from_str() { - assert_eq!("0".parse::().unwrap(), 0); - assert!("foo".parse::().is_err()); + fn from_str_degree_post_distribution() { + assert!(parse("5°209999′1007″0‴").is_ok()); + assert!(parse("5°0′1008″0‴").is_err()); } #[test] diff --git a/src/subcommand/traits.rs b/src/subcommand/traits.rs index dc866e064f..917f08ec9e 100644 --- a/src/subcommand/traits.rs +++ b/src/subcommand/traits.rs @@ -11,17 +11,12 @@ impl Traits { return Err(anyhow!("Invalid ordinal")); } - println!("name: {}", self.ordinal.name()); - - println!("cycle: {}", self.ordinal.cycle()); - - println!("epoch: {}", self.ordinal.epoch()); - - println!("height: {}", self.ordinal.height()); - - println!("period: {}", self.ordinal.period()); - - println!("offset: {}", self.ordinal.subsidy_position()); + println!("number: {}", self.ordinal.n()); + println!( + "decimal: {}.{}", + self.ordinal.height(), + self.ordinal.subsidy_position() + ); let height = self.ordinal.height().n(); let c = height / (CYCLE_EPOCHS * Epoch::BLOCKS); @@ -30,6 +25,13 @@ impl Traits { let o = self.ordinal.subsidy_position(); println!("degree: {c}°{e}′{p}″{o}‴"); + println!("name: {}", self.ordinal.name()); + println!("height: {}", self.ordinal.height()); + println!("cycle: {}", self.ordinal.cycle()); + println!("epoch: {}", self.ordinal.epoch()); + println!("period: {}", self.ordinal.period()); + println!("offset: {}", self.ordinal.subsidy_position()); + println!( "rarity: {}", if c == 0 && o == 0 && p == 0 && e == 0 { diff --git a/tests/lib.rs b/tests/lib.rs index bdac4e089a..9a6e3a93e7 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -89,7 +89,7 @@ struct Test { envs: Vec<(OsString, OsString)>, events: Vec, expected_status: i32, - expected_stderr: Option, + expected_stderr: Expected, expected_stdout: Expected, tempdir: TempDir, } @@ -105,7 +105,7 @@ impl Test { envs: Vec::new(), events: Vec::new(), expected_status: 0, - expected_stderr: None, + expected_stderr: Expected::Ignore, expected_stdout: Expected::String(String::new()), tempdir, } @@ -139,7 +139,7 @@ impl Test { fn stdout_regex(self, expected_stdout: impl AsRef) -> Self { Self { expected_stdout: Expected::Regex( - Regex::new(&format!("^{}$", expected_stdout.as_ref())).unwrap(), + Regex::new(&format!("(?s)^{}$", expected_stdout.as_ref())).unwrap(), ), ..self } @@ -155,7 +155,16 @@ impl Test { fn expected_stderr(self, expected_stderr: &str) -> Self { Self { - expected_stderr: Some(expected_stderr.to_owned()), + expected_stderr: Expected::String(expected_stderr.to_owned()), + ..self + } + } + + fn stderr_regex(self, expected_stderr: impl AsRef) -> Self { + Self { + expected_stderr: Expected::Regex( + Regex::new(&format!("(?s)^{}$", expected_stderr.as_ref())).unwrap(), + ), ..self } } @@ -219,7 +228,7 @@ impl Test { .envs(self.envs) .stdin(Stdio::null()) .stdout(Stdio::piped()) - .stderr(if self.expected_stderr.is_some() { + .stderr(if !matches!(self.expected_stderr, Expected::Ignore) { Stdio::piped() } else { Stdio::inherit() @@ -291,21 +300,29 @@ impl Test { ); } - let re = Regex::new(r"(?m)^\[.*\n")?; + let log_line_re = Regex::new(r"(?m)^\[.*\n")?; - for m in re.find_iter(stderr) { - print!("{}", m.as_str()) + for log_line in log_line_re.find_iter(stderr) { + print!("{}", log_line.as_str()) } - if let Some(expected_stderr) = self.expected_stderr { - assert_eq!(re.replace_all(stderr, ""), expected_stderr); + let stripped_stderr = log_line_re.replace_all(stderr, ""); + + match self.expected_stderr { + Expected::String(expected_stderr) => assert_eq!(stripped_stderr, expected_stderr), + Expected::Regex(expected_stderr) => assert!( + expected_stderr.is_match(&stripped_stderr), + "stderr did not match regex: {:?}", + stripped_stderr + ), + Expected::Ignore => {} } match self.expected_stdout { Expected::String(expected_stdout) => assert_eq!(stdout, expected_stdout), Expected::Regex(expected_stdout) => assert!( expected_stdout.is_match(stdout), - "stdout did not match regex: {}", + "stdout did not match regex: {:?}", stdout ), Expected::Ignore => {} diff --git a/tests/traits.rs b/tests/traits.rs index 5445a0db88..54c16175ec 100644 --- a/tests/traits.rs +++ b/tests/traits.rs @@ -25,8 +25,8 @@ fn case(ordinal: u64, name: &str, value: &str) { fn invalid_ordinal() -> Result { Test::new()? .args(&["traits", "2099999997690000"]) - .expected_stderr("error: Invalid ordinal\n") - .expected_status(1) + .stderr_regex("error: Invalid value \"2099999997690000\" for '': Invalid ordinal\n.*") + .expected_status(2) .run() } @@ -42,6 +42,16 @@ fn name() { case(27, "name", "nvtdijuwxko"); } +#[test] +fn number() { + case(2099999997689999, "number", "2099999997689999"); +} + +#[test] +fn decimal() { + case(2099999997689999, "decimal", "6929999.0"); +} + #[test] fn height() { case(0, "height", "0"); @@ -70,9 +80,12 @@ fn epoch() { #[test] fn period() { case(0, "period", "0"); - case(2015, "period", "0"); - case(2016, "period", "1"); - case(2017, "period", "1"); + case(10075000000000, "period", "0"); + case(10080000000000 - 1, "period", "0"); + case(10080000000000, "period", "1"); + case(10080000000000 + 1, "period", "1"); + case(10085000000000, "period", "1"); + case(2099999997689999, "period", "3437"); } #[test]