From 2115bf26ba60f467b81b563cb8d0b5177fe08e75 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 22 Nov 2024 14:01:58 -0800 Subject: [PATCH 1/3] Add function to calculate rune unlock height (#4097) --- crates/ordinals/src/rune.rs | 244 ++++++++++++++++++++++++++++-------- 1 file changed, 189 insertions(+), 55 deletions(-) diff --git a/crates/ordinals/src/rune.rs b/crates/ordinals/src/rune.rs index 90e26865cf..c95a215b96 100644 --- a/crates/ordinals/src/rune.rs +++ b/crates/ordinals/src/rune.rs @@ -8,6 +8,10 @@ pub struct Rune(pub u128); impl Rune { const RESERVED: u128 = 6402364363415443603228541259936211926; + const UNLOCKED: usize = 12; + + const UNLOCK_INTERVAL: u32 = SUBSIDY_HALVING_INTERVAL / 12; + const STEPS: &'static [u128] = &[ 0, 26, @@ -54,17 +58,15 @@ impl Rune { } } - pub fn minimum_at_height(chain: Network, height: Height) -> Self { + pub fn minimum_at_height(network: Network, height: Height) -> Self { let offset = height.0.saturating_add(1); - const INTERVAL: u32 = SUBSIDY_HALVING_INTERVAL / 12; - - let start = Self::first_rune_height(chain); + let start = Self::first_rune_height(network); let end = start + SUBSIDY_HALVING_INTERVAL; if offset < start { - return Rune(Self::STEPS[12]); + return Rune(Self::STEPS[Self::UNLOCKED]); } if offset >= end { @@ -73,15 +75,41 @@ impl Rune { let progress = offset.saturating_sub(start); - let length = 12u32.saturating_sub(progress / INTERVAL); + let length = u32::try_from(Self::UNLOCKED) + .unwrap() + .saturating_sub(progress / Self::UNLOCK_INTERVAL); let end = Self::STEPS[usize::try_from(length - 1).unwrap()]; let start = Self::STEPS[usize::try_from(length).unwrap()]; - let remainder = u128::from(progress % INTERVAL); + let remainder = u128::from(progress % Self::UNLOCK_INTERVAL); + + Rune(start - ((start - end) * remainder / u128::from(Self::UNLOCK_INTERVAL))) + } + + pub fn unlock_height(self, network: Network) -> Option { + if self.is_reserved() { + return None; + } + + if self.0 >= Self::STEPS[Self::UNLOCKED] { + return Some(Height(0)); + } + + let i = Self::STEPS.iter().position(|&step| self.0 < step).unwrap(); + + let start = Self::STEPS[i]; + let end = i.checked_sub(1).map(|i| Self::STEPS[i]).unwrap_or_default(); - Rune(start - ((start - end) * remainder / u128::from(INTERVAL))) + let interval = start - end; + let progress = start - self.0; + + let height = Self::first_rune_height(network) + + u32::try_from(Self::UNLOCKED - i).unwrap() * Self::UNLOCK_INTERVAL + + u32::try_from((progress * u128::from(Self::UNLOCK_INTERVAL) - 1) / interval).unwrap(); + + Some(Height(height)) } pub fn is_reserved(self) -> bool { @@ -240,15 +268,23 @@ mod tests { fn mainnet_minimum_at_height() { #[track_caller] fn case(height: u32, minimum: &str) { + let minimum = minimum.parse::().unwrap(); assert_eq!( - Rune::minimum_at_height(Network::Bitcoin, Height(height)).to_string(), + Rune::minimum_at_height(Network::Bitcoin, Height(height)), minimum, ); + + let unlock_height = minimum.unlock_height(Network::Bitcoin).unwrap().0; + + assert!(unlock_height <= height); + + if unlock_height == 0 { + assert!(height < SUBSIDY_HALVING_INTERVAL * 4); + } } const START: u32 = SUBSIDY_HALVING_INTERVAL * 4; const END: u32 = START + SUBSIDY_HALVING_INTERVAL; - const INTERVAL: u32 = SUBSIDY_HALVING_INTERVAL / 12; case(0, "AAAAAAAAAAAAA"); case(START / 2, "AAAAAAAAAAAAA"); @@ -259,64 +295,70 @@ mod tests { case(END + 1, "A"); case(u32::MAX, "A"); - case(START + INTERVAL * 00 - 1, "AAAAAAAAAAAAA"); - case(START + INTERVAL * 00 + 0, "ZZYZXBRKWXVA"); - case(START + INTERVAL * 00 + 1, "ZZXZUDIVTVQA"); + case(START + Rune::UNLOCK_INTERVAL * 00 - 1, "AAAAAAAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 00 + 0, "ZZYZXBRKWXVA"); + case(START + Rune::UNLOCK_INTERVAL * 00 + 1, "ZZXZUDIVTVQA"); - case(START + INTERVAL * 01 - 1, "AAAAAAAAAAAA"); - case(START + INTERVAL * 01 + 0, "ZZYZXBRKWXV"); - case(START + INTERVAL * 01 + 1, "ZZXZUDIVTVQ"); + case(START + Rune::UNLOCK_INTERVAL * 01 - 1, "AAAAAAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 01 + 0, "ZZYZXBRKWXV"); + case(START + Rune::UNLOCK_INTERVAL * 01 + 1, "ZZXZUDIVTVQ"); - case(START + INTERVAL * 02 - 1, "AAAAAAAAAAA"); - case(START + INTERVAL * 02 + 0, "ZZYZXBRKWY"); - case(START + INTERVAL * 02 + 1, "ZZXZUDIVTW"); + case(START + Rune::UNLOCK_INTERVAL * 02 - 1, "AAAAAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 02 + 0, "ZZYZXBRKWY"); + case(START + Rune::UNLOCK_INTERVAL * 02 + 1, "ZZXZUDIVTW"); - case(START + INTERVAL * 03 - 1, "AAAAAAAAAA"); - case(START + INTERVAL * 03 + 0, "ZZYZXBRKX"); - case(START + INTERVAL * 03 + 1, "ZZXZUDIVU"); + case(START + Rune::UNLOCK_INTERVAL * 03 - 1, "AAAAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 03 + 0, "ZZYZXBRKX"); + case(START + Rune::UNLOCK_INTERVAL * 03 + 1, "ZZXZUDIVU"); - case(START + INTERVAL * 04 - 1, "AAAAAAAAA"); - case(START + INTERVAL * 04 + 0, "ZZYZXBRL"); - case(START + INTERVAL * 04 + 1, "ZZXZUDIW"); + case(START + Rune::UNLOCK_INTERVAL * 04 - 1, "AAAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 04 + 0, "ZZYZXBRL"); + case(START + Rune::UNLOCK_INTERVAL * 04 + 1, "ZZXZUDIW"); - case(START + INTERVAL * 05 - 1, "AAAAAAAA"); - case(START + INTERVAL * 05 + 0, "ZZYZXBS"); - case(START + INTERVAL * 05 + 1, "ZZXZUDJ"); + case(START + Rune::UNLOCK_INTERVAL * 05 - 1, "AAAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 05 + 0, "ZZYZXBS"); + case(START + Rune::UNLOCK_INTERVAL * 05 + 1, "ZZXZUDJ"); - case(START + INTERVAL * 06 - 1, "AAAAAAA"); - case(START + INTERVAL * 06 + 0, "ZZYZXC"); - case(START + INTERVAL * 06 + 1, "ZZXZUE"); + case(START + Rune::UNLOCK_INTERVAL * 06 - 1, "AAAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 06 + 0, "ZZYZXC"); + case(START + Rune::UNLOCK_INTERVAL * 06 + 1, "ZZXZUE"); - case(START + INTERVAL * 07 - 1, "AAAAAA"); - case(START + INTERVAL * 07 + 0, "ZZYZY"); - case(START + INTERVAL * 07 + 1, "ZZXZV"); + case(START + Rune::UNLOCK_INTERVAL * 07 - 1, "AAAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 07 + 0, "ZZYZY"); + case(START + Rune::UNLOCK_INTERVAL * 07 + 1, "ZZXZV"); - case(START + INTERVAL * 08 - 1, "AAAAA"); - case(START + INTERVAL * 08 + 0, "ZZZA"); - case(START + INTERVAL * 08 + 1, "ZZYA"); + case(START + Rune::UNLOCK_INTERVAL * 08 - 1, "AAAAA"); + case(START + Rune::UNLOCK_INTERVAL * 08 + 0, "ZZZA"); + case(START + Rune::UNLOCK_INTERVAL * 08 + 1, "ZZYA"); - case(START + INTERVAL * 09 - 1, "AAAA"); - case(START + INTERVAL * 09 + 0, "ZZZ"); - case(START + INTERVAL * 09 + 1, "ZZY"); + case(START + Rune::UNLOCK_INTERVAL * 09 - 1, "AAAA"); + case(START + Rune::UNLOCK_INTERVAL * 09 + 0, "ZZZ"); + case(START + Rune::UNLOCK_INTERVAL * 09 + 1, "ZZY"); - case(START + INTERVAL * 10 - 2, "AAC"); - case(START + INTERVAL * 10 - 1, "AAA"); - case(START + INTERVAL * 10 + 0, "AAA"); - case(START + INTERVAL * 10 + 1, "AAA"); + case(START + Rune::UNLOCK_INTERVAL * 10 - 2, "AAC"); + case(START + Rune::UNLOCK_INTERVAL * 10 - 1, "AAA"); + case(START + Rune::UNLOCK_INTERVAL * 10 + 0, "AAA"); + case(START + Rune::UNLOCK_INTERVAL * 10 + 1, "AAA"); - case(START + INTERVAL * 10 + INTERVAL / 2, "NA"); + case( + START + Rune::UNLOCK_INTERVAL * 10 + Rune::UNLOCK_INTERVAL / 2, + "NA", + ); - case(START + INTERVAL * 11 - 2, "AB"); - case(START + INTERVAL * 11 - 1, "AA"); - case(START + INTERVAL * 11 + 0, "AA"); - case(START + INTERVAL * 11 + 1, "AA"); + case(START + Rune::UNLOCK_INTERVAL * 11 - 2, "AB"); + case(START + Rune::UNLOCK_INTERVAL * 11 - 1, "AA"); + case(START + Rune::UNLOCK_INTERVAL * 11 + 0, "AA"); + case(START + Rune::UNLOCK_INTERVAL * 11 + 1, "AA"); - case(START + INTERVAL * 11 + INTERVAL / 2, "N"); + case( + START + Rune::UNLOCK_INTERVAL * 11 + Rune::UNLOCK_INTERVAL / 2, + "N", + ); - case(START + INTERVAL * 12 - 2, "B"); - case(START + INTERVAL * 12 - 1, "A"); - case(START + INTERVAL * 12 + 0, "A"); - case(START + INTERVAL * 12 + 1, "A"); + case(START + Rune::UNLOCK_INTERVAL * 12 - 2, "B"); + case(START + Rune::UNLOCK_INTERVAL * 12 - 1, "A"); + case(START + Rune::UNLOCK_INTERVAL * 12 + 0, "A"); + case(START + Rune::UNLOCK_INTERVAL * 12 + 1, "A"); } #[test] @@ -382,7 +424,9 @@ mod tests { fn is_reserved() { #[track_caller] fn case(rune: &str, reserved: bool) { - assert_eq!(rune.parse::().unwrap().is_reserved(), reserved); + let rune = rune.parse::().unwrap(); + assert_eq!(rune.is_reserved(), reserved); + assert_eq!(rune.unlock_height(Network::Bitcoin).is_none(), reserved); } case("A", false); @@ -420,4 +464,94 @@ mod tests { case(65536, &[0, 0, 1]); case(u128::MAX, &[255; 16]); } + + #[test] + fn steps_are_sorted_and_unique() { + let mut steps = Rune::STEPS.to_vec(); + steps.sort(); + assert_eq!(steps, Rune::STEPS); + steps.dedup(); + assert_eq!(steps, Rune::STEPS); + } + + #[test] + fn reserved_rune_unlock_height() { + assert_eq!(Rune(Rune::RESERVED).unlock_height(Network::Bitcoin), None); + assert_eq!( + Rune(Rune::RESERVED + 1).unlock_height(Network::Bitcoin), + None + ); + assert_eq!( + Rune(Rune::RESERVED - 1).unlock_height(Network::Bitcoin), + Some(Height(0)) + ); + } + + #[test] + fn unlock_height() { + #[track_caller] + fn case(rune: &str, unlock_height: u32) { + let rune = rune.parse::().unwrap(); + assert_eq!( + rune.unlock_height(Network::Bitcoin), + Some(Height(unlock_height)), + "invalid unlock height for rune `{rune}`", + ); + + if unlock_height > 0 { + assert!(rune >= Rune::minimum_at_height(Network::Bitcoin, Height(unlock_height))); + assert!(rune < Rune::minimum_at_height(Network::Bitcoin, Height(unlock_height - 1))); + } + } + + const START: u32 = SUBSIDY_HALVING_INTERVAL * 4; + + case("AAAAAAAAAAAAB", 0); + + case("AAAAAAAAAAAAA", 0); + + case("ZZZZZZZZZZZZ", START); + + case("ZZZZZZZZZZZ", START + Rune::UNLOCK_INTERVAL); + + case("ZZZZZZZZZZ", START + Rune::UNLOCK_INTERVAL * 2); + + case("ZZZZZZZZZ", START + Rune::UNLOCK_INTERVAL * 3); + + case("ZZYZXBRKWXVA", START); + + case("ZZZ", 997_500); + + case("AAA", 1_014_999); + + case("NNNN", 988_400); + + case("Z", 1_033_173); + case("Y", 1_033_846); + case("P", 1_039_903); + case("O", 1_040_576); + case("N", 1_041_249); + case("M", 1_041_923); + case("L", 1_042_596); + case("K", 1_043_269); + case("J", 1_043_942); + case("I", 1_044_615); + case("H", 1_045_288); + case("G", 1_045_961); + case("F", 1_046_634); + case("E", 1_047_307); + case("D", 1_047_980); + case("C", 1_048_653); + case("B", 1_049_326); + case("A", 1_049_999); + + for i in 0..4 { + for n in Rune::STEPS[i]..Rune::STEPS[i + 1] { + let rune = Rune(n); + let unlock_height = rune.unlock_height(Network::Bitcoin).unwrap(); + assert!(rune >= Rune::minimum_at_height(Network::Bitcoin, unlock_height)); + assert!(rune < Rune::minimum_at_height(Network::Bitcoin, Height(unlock_height.0 - 1))); + } + } + } } From 53753e3413e2ae8a129084e3e31338f763d4baa6 Mon Sep 17 00:00:00 2001 From: raph Date: Sat, 23 Nov 2024 00:23:33 +0100 Subject: [PATCH 2/3] Show rune unlock height (#3580) --- Cargo.lock | 126 ++++++++++++++++++-------------- crates/ordinals/src/rune.rs | 2 +- src/subcommand/server.rs | 75 ++++++++++++++++--- src/templates.rs | 2 + src/templates/rune_not_found.rs | 54 ++++++++++++++ templates/rune-not-found.html | 14 ++++ 6 files changed, 207 insertions(+), 66 deletions(-) create mode 100644 src/templates/rune_not_found.rs create mode 100644 templates/rune-not-found.html diff --git a/Cargo.lock b/Cargo.lock index 1ab5ffe0b4..0fd08f6cf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "atom_syndication" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3a5ed3201df5658d1aa45060c5a57dc9dba8a8ada20d696d67cb0c479ee043" +checksum = "3ee79fb83c725eae67b55218870813d2fc39fd85e4f1583848ef9f4f823cfe7c" dependencies = [ "chrono", "derive_builder", @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "serde", @@ -703,9 +703,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.37" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -772,9 +772,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -782,9 +782,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -806,9 +806,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "colorchoice" @@ -844,7 +844,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -1104,9 +1104,9 @@ dependencies = [ [[package]] name = "diligent-date-parser" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182" +checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9" dependencies = [ "chrono", ] @@ -1253,9 +1253,9 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1511,9 +1511,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -1713,14 +1713,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.6", + "h2 0.4.7", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -1739,9 +1739,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", - "rustls 0.23.16", + "rustls 0.23.17", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1769,7 +1769,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "native-tls", "tokio", @@ -1788,7 +1788,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -1988,15 +1988,15 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -2053,9 +2053,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "js-sys" @@ -2156,9 +2156,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libredox" @@ -2823,9 +2823,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" dependencies = [ "encoding_rs", "memchr", @@ -3034,11 +3034,11 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.6", + "h2 0.4.7", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -3054,7 +3054,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "system-configuration 0.6.1", "tokio", "tokio-native-tls", @@ -3098,9 +3098,9 @@ dependencies = [ [[package]] name = "rss" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e92048f840d98c6d6dd870af9101610ea9ff413f11f1bcebf4f4c31d96d957" +checksum = "554a62b3dd5450fcbb0435b3db809f9dd3c6e9f5726172408f7ad3b57ed59057" dependencies = [ "atom_syndication", "derive_builder", @@ -3173,9 +3173,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -3212,9 +3212,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" dependencies = [ "once_cell", "rustls-pki-types", @@ -3320,9 +3320,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3420,9 +3420,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "indexmap 2.6.0", "itoa", @@ -3670,9 +3670,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -3765,7 +3765,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.2.0", "once_cell", - "rustix 0.38.40", + "rustix 0.38.41", "windows-sys 0.59.0", ] @@ -3928,7 +3928,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.16", + "rustls 0.23.17", "rustls-pki-types", "tokio", ] @@ -4072,9 +4072,9 @@ checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" @@ -4091,6 +4091,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4289,6 +4295,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/crates/ordinals/src/rune.rs b/crates/ordinals/src/rune.rs index c95a215b96..ab052c045a 100644 --- a/crates/ordinals/src/rune.rs +++ b/crates/ordinals/src/rune.rs @@ -6,7 +6,7 @@ use super::*; pub struct Rune(pub u128); impl Rune { - const RESERVED: u128 = 6402364363415443603228541259936211926; + pub const RESERVED: u128 = 6402364363415443603228541259936211926; const UNLOCKED: usize = 12; diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 196e841501..38e501f4ed 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -10,7 +10,7 @@ use { InputHtml, InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, - PreviewVideoHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, TransactionHtml, + PreviewVideoHtml, RareTxt, RuneHtml, RuneNotFoundHtml, RunesHtml, SatHtml, TransactionHtml, }, axum::{ body, @@ -807,9 +807,21 @@ impl Server { .ok_or_not_found(|| format!("rune number {number}"))?, }; - let (id, entry, parent) = index - .rune(rune)? - .ok_or_not_found(|| format!("rune {rune}"))?; + let Some((id, entry, parent)) = index.rune(rune)? else { + return Ok(if accept_json { + StatusCode::NOT_FOUND.into_response() + } else { + ( + StatusCode::NOT_FOUND, + RuneNotFoundHtml { + rune, + unlock_height: rune.unlock_height(server_config.chain.network()), + } + .page(server_config), + ) + .into_response() + }); + }; let block_height = index.block_height()?.unwrap_or(Height(0)); @@ -2590,14 +2602,19 @@ mod tests { #[track_caller] fn assert_html(&self, path: impl AsRef, content: impl PageContent) { + self.assert_html_status(path, StatusCode::OK, content); + } + + #[track_caller] + fn assert_html_status( + &self, + path: impl AsRef, + status: StatusCode, + content: impl PageContent, + ) { let response = self.get(path); - assert_eq!( - response.status(), - StatusCode::OK, - "{}", - response.text().unwrap() - ); + assert_eq!(response.status(), status, "{}", response.text().unwrap()); let expected_response = PageHtml::new( content, @@ -3209,6 +3226,44 @@ mod tests { ); } + #[test] + fn rune_not_etched_shows_unlock_height() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_runes() + .build(); + + server.mine_blocks(1); + + server.assert_html_status( + "/rune/A", + StatusCode::NOT_FOUND, + RuneNotFoundHtml { + rune: Rune(0), + unlock_height: Some(Height(209999)), + }, + ); + } + + #[test] + fn reserved_rune_not_etched_shows_reserved_status() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_runes() + .build(); + + server.mine_blocks(1); + + server.assert_html_status( + format!("/rune/{}", Rune(Rune::RESERVED)), + StatusCode::NOT_FOUND, + RuneNotFoundHtml { + rune: Rune(Rune::RESERVED), + unlock_height: None, + }, + ); + } + #[test] fn runes_are_displayed_on_runes_page() { let server = TestServer::builder() diff --git a/src/templates.rs b/src/templates.rs index 7898d2a5f8..fa54b01725 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -20,6 +20,7 @@ pub(crate) use { PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, }, rare::RareTxt, + rune_not_found::RuneNotFoundHtml, sat::SatHtml, }; @@ -46,6 +47,7 @@ mod parents; mod preview; mod rare; pub mod rune; +pub mod rune_not_found; pub mod runes; pub mod sat; pub mod status; diff --git a/src/templates/rune_not_found.rs b/src/templates/rune_not_found.rs new file mode 100644 index 0000000000..e050fa5c80 --- /dev/null +++ b/src/templates/rune_not_found.rs @@ -0,0 +1,54 @@ +use super::*; + +#[derive(Boilerplate, Debug, PartialEq, Serialize)] +pub struct RuneNotFoundHtml { + pub rune: Rune, + pub unlock_height: Option, +} + +impl PageContent for RuneNotFoundHtml { + fn title(&self) -> String { + format!("Rune {}", self.rune) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display() { + assert_regex_match!( + RuneNotFoundHtml { + rune: Rune(u128::MAX), + unlock_height: Some(Height(111)), + }, + "

BCGDENLQRQWDSLRUGSNLBTMFIJAV

+
+
unlock height
+
111
+
reserved
+
false
+
+" + ); + } + + #[test] + fn display_reserved() { + assert_regex_match!( + RuneNotFoundHtml { + rune: Rune(Rune::RESERVED), + unlock_height: None, + }, + "

AAAAAAAAAAAAAAAAAAAAAAAAAAA

+
+
unlock height
+
none
+
reserved
+
true
+
+" + ); + } +} diff --git a/templates/rune-not-found.html b/templates/rune-not-found.html new file mode 100644 index 0000000000..ede0843104 --- /dev/null +++ b/templates/rune-not-found.html @@ -0,0 +1,14 @@ +

{{ self.rune }}

+
+%% if let Some(unlock_height) = self.unlock_height { +
unlock height
+
{{ unlock_height }}
+
reserved
+
false
+%% } else { +
unlock height
+
none
+
reserved
+
true
+%% } +
From 446d48521fdb968dddb3d95f8beb63028015f7b2 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 22 Nov 2024 15:46:09 -0800 Subject: [PATCH 3/3] DocumentiInscription URIs (#4098) --- docs/src/SUMMARY.md | 1 + docs/src/inscriptions/examples.md | 68 ++++++++++++++++--------------- docs/src/inscriptions/uris.md | 40 ++++++++++++++++++ 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 docs/src/inscriptions/uris.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fedeecea3f..6f535527e1 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -11,6 +11,7 @@ Summary - [Provenance](inscriptions/provenance.md) - [Recursion](inscriptions/recursion.md) - [Rendering](inscriptions/rendering.md) + - [URIs](inscriptions/uris.md) - [Examples](inscriptions/examples.md) - [Runes](runes.md) - [Specification](runes/specification.md) diff --git a/docs/src/inscriptions/examples.md b/docs/src/inscriptions/examples.md index 12dc0417e7..05d8555466 100644 --- a/docs/src/inscriptions/examples.md +++ b/docs/src/inscriptions/examples.md @@ -1,32 +1,36 @@ -Inscription Examples -========= - -### Delegate - -* The [first delegate inscription](https://ordinals.com/inscription/626127511953479e1933a448f020c76acd9086efe2f2b3f5e492c0c85d2ce051i0). -* The Oscillations * collection utilizes delegation, provenance, recursion, sat endpoint, and detects the kind of sat that each piece is inscribed on (sattribute-aware). Each piece is a delegate of [this inscription](https://ordinals.com/inscription/52b4ea10c2518c954c73594e403ccfb2d50044f5a3b09a224dfa3bf06dd1d499i0). -* [This inscription](https://ordinals.com/inscription/23a8f17fff4a73e2932dfc76e46d14d4f8975da96f5d5ae9a45898422056071ai0) was inscribed as a delegate of [this inscription](https://ordinals.com/inscription/9ff39db4c51f831225d41efbd29a399f2b16c758970ec4ab95a1a17e8be59088i0) and is also the parent inscription of a rune. - -### Metadata - -* Each [member](https://ordinals.com/inscription/ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1) in the FUN collection has metadata that describes its attributes. -* [This inscription](https://ordinals.com/inscription/454700af2cffd3a872daeb89598a891f5fe9936225838a8c4522f491f246ca0ai0) uses its own metadata to draw the ordinal image. - -### Provenance - -* [Inscription 0](https://ordinals.com/inscription/0) is the parent inscription for Casey's sugar skull collection, a grandparent for the FUN! collection, and the grandparent for [the sleepiest rune](https://ordinals.com/rune/ZZZZZZZZZZZZZZZZZZZZZZZZZZ). -* With the [Rug Me](https://ordinals.com/inscription/f87a6b16a1224b65f9c8ea55e1718585b3075373581689e343860c12a70d15c7i41) collection, owners are able to change the background color by inscribing a child to it. -* This [Bitcoin Magazine Cover](https://ordinals.com/inscription/60902330) renders the children as part of the parent inscription. -* [The yellow_ord_bot](https://ordinals.com/inscription/17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0) has many different quotes as cursed children. -* The [Spellbound](https://ordinals.com/children/451accbce30177c23a8cd42ab85131312a449359ada3c054aa147b054136fa3bi0) collection from the Wizard of Ord utilizes recursion, delegation, metadata, provenance, postage, location, compression. - -### Recursion - -* [Inscription 12992](https://ordinals.com/inscription/bf637552f3758e20f733d1f250cbea7cbbb4bbf157a8d4a9c26132950a383415i0) was the first recursive inscription inscribed on mainnet. -* [OnChain Monkey Genesis (BTC)](https://ordinals.com/children/fb162a46943e5d7d31d72ee2c8c850e66c1ca5d0d453068aa63883528285ed21i0) was one of the earliest collections to use recursion to create its PFP art. -* [Blob](https://ordinals.com/inscription/67285791) is a recursive generative collection that seeds its generation with metadata and uses threeJS, React 3 Fiber and other libraries recursively. -* The [GPU Ordinals](https://ordinals.com/inscription/0b62d8790bb428e0278cd8c3dedd540e2495515198002a233a49ba7b21f8b2cei0) collection takes recursive content and transforms it before rendering, creating what is termed as 'super-recursion'. Use Google Chrome and headphones to experience the spatial audio. -* The [Abstractii Genesis](https://ordinals.com/inscription/b2de70a5658d3659b74a9a349e02ed9396318eab844073b6f35e726dea9e103ei0) collection uses the inscriptions ID as a seed to generate its art. -* The [Abstractii Evolved](https://ordinals.com/inscription/c56795a454a30bb6866686770f14d015d53f3cf2ddfa5154e34a7cd1120a51efi0) generative collection uses the recursive blockheight endpoint as a seed to generate its art. -* [This code](http://ordinals.com/content/eafb859825cd843587d39552eb7a52f352e9621cd16b63b8702c1b8ea44faf1ci0) is called recursively in [this inscription](https://ordinals.com/inscription/60445754) to generate music. -* [This code](https://ordinals.com/content/e48af8aebe608656a3739393d1270d88285ab3051fb800743509d82bcf163623i0) is called recursively in [this inscription](https://ordinals.com/inscription/70940369), allowing it to function as a pixel art drawing program. +Inscription Examples +==================== + +Delegate +-------- + +* The [first delegate inscription](https://ordinals.com/inscription/626127511953479e1933a448f020c76acd9086efe2f2b3f5e492c0c85d2ce051i0). +* The Oscillations * collection utilizes delegation, provenance, recursion, sat endpoint, and detects the kind of sat that each piece is inscribed on (sattribute-aware). Each piece is a delegate of [this inscription](https://ordinals.com/inscription/52b4ea10c2518c954c73594e403ccfb2d50044f5a3b09a224dfa3bf06dd1d499i0). +* [This inscription](https://ordinals.com/inscription/23a8f17fff4a73e2932dfc76e46d14d4f8975da96f5d5ae9a45898422056071ai0) was inscribed as a delegate of [this inscription](https://ordinals.com/inscription/9ff39db4c51f831225d41efbd29a399f2b16c758970ec4ab95a1a17e8be59088i0) and is also the parent inscription of a rune. + +Metadata +-------- + +* Each [member](https://ordinals.com/inscription/ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1) in the FUN collection has metadata that describes its attributes. +* [This inscription](https://ordinals.com/inscription/454700af2cffd3a872daeb89598a891f5fe9936225838a8c4522f491f246ca0ai0) uses its own metadata to draw the ordinal image. + +Provenance +---------- + +* [Inscription 0](https://ordinals.com/inscription/0) is the parent inscription for Casey's sugar skull collection, a grandparent for the FUN! collection, and the grandparent for [the sleepiest rune](https://ordinals.com/rune/ZZZZZZZZZZZZZZZZZZZZZZZZZZ). +* With the [Rug Me](https://ordinals.com/inscription/f87a6b16a1224b65f9c8ea55e1718585b3075373581689e343860c12a70d15c7i41) collection, owners are able to change the background color by inscribing a child to it. +* This [Bitcoin Magazine Cover](https://ordinals.com/inscription/60902330) renders the children as part of the parent inscription. +* [The yellow_ord_bot](https://ordinals.com/inscription/17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0) has many different quotes as cursed children. +* The [Spellbound](https://ordinals.com/children/451accbce30177c23a8cd42ab85131312a449359ada3c054aa147b054136fa3bi0) collection from the Wizard of Ord utilizes recursion, delegation, metadata, provenance, postage, location, compression. + +Recursion +--------- + +* [Inscription 12992](https://ordinals.com/inscription/bf637552f3758e20f733d1f250cbea7cbbb4bbf157a8d4a9c26132950a383415i0) was the first recursive inscription inscribed on mainnet. +* [OnChain Monkey Genesis (BTC)](https://ordinals.com/children/fb162a46943e5d7d31d72ee2c8c850e66c1ca5d0d453068aa63883528285ed21i0) was one of the earliest collections to use recursion to create its PFP art. +* [Blob](https://ordinals.com/inscription/67285791) is a recursive generative collection that seeds its generation with metadata and uses threeJS, React 3 Fiber and other libraries recursively. +* The [GPU Ordinals](https://ordinals.com/inscription/0b62d8790bb428e0278cd8c3dedd540e2495515198002a233a49ba7b21f8b2cei0) collection takes recursive content and transforms it before rendering, creating what is termed as 'super-recursion'. Use Google Chrome and headphones to experience the spatial audio. +* The [Abstractii Genesis](https://ordinals.com/inscription/b2de70a5658d3659b74a9a349e02ed9396318eab844073b6f35e726dea9e103ei0) collection uses the inscriptions ID as a seed to generate its art. +* The [Abstractii Evolved](https://ordinals.com/inscription/c56795a454a30bb6866686770f14d015d53f3cf2ddfa5154e34a7cd1120a51efi0) generative collection uses the recursive blockheight endpoint as a seed to generate its art. +* [This code](http://ordinals.com/content/eafb859825cd843587d39552eb7a52f352e9621cd16b63b8702c1b8ea44faf1ci0) is called recursively in [this inscription](https://ordinals.com/inscription/60445754) to generate music. +* [This code](https://ordinals.com/content/e48af8aebe608656a3739393d1270d88285ab3051fb800743509d82bcf163623i0) is called recursively in [this inscription](https://ordinals.com/inscription/70940369), allowing it to function as a pixel art drawing program. diff --git a/docs/src/inscriptions/uris.md b/docs/src/inscriptions/uris.md new file mode 100644 index 0000000000..d5eeb12b83 --- /dev/null +++ b/docs/src/inscriptions/uris.md @@ -0,0 +1,40 @@ +URIs +==== + +*This document is a draft. It should be considered provisional and subject to +change at any time. The `ord:` schema has not been registered with the IANA.* + +Inscriptions content can be addressed with inscription URIs using the `ord:` +schema. + +Inscription URIs consist of `ord:` followed by a target inscription ID. `ord:` +is not followed by `//`, since the schema-specific part of inscription URIs, +namely the target inscription ID, does not contain a hierarchical structure. + +For example, the inscription URI of the genesis inscription is: + +``` +ord:6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 +``` + +Inscription URIs match the following verbose regular expression: + +``` +(?i) # case-insensitive +ord: # schema +[0-9a-f]{64} # transaction ID +i # separator +(0|[1-9][0-9]*) # inscription index +``` + +Inscription URIs are case-insensitive and can thus use the more compact +alphanumeric mode when encoded as QR codes. Lowercase is, however, the +preferred presentation style. + +The referent of an inscription URI is an HTTP resource with the content, +content type, content encoding, and content length corresponding to the +inscription with the given ID. + +The referent of an inscription URI is always the original content of the target +inscription, and not the content of the delegate, regardless of whether or not +the target inscription has a delegate.