diff --git a/Cargo.lock b/Cargo.lock index 2ddd19578b..83f32914e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,6 +2185,7 @@ dependencies = [ "miniscript", "mp4", "ord-bitcoincore-rpc", + "ordinals", "pretty_assertions", "pulldown-cmark", "redb", @@ -2234,6 +2235,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ordinals" +version = "0.0.1" +dependencies = [ + "bitcoin", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "parking" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 523bd41fcb..e749645369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ copyright = "The Ord Maintainers" maintainer = "The Ord Maintainers" [workspace] -members = [".", "test-bitcoincore-rpc", "crates/*"] +members = [".", "crates/*"] [dependencies] anyhow = { version = "1.0.56", features = ["backtrace"] } @@ -49,6 +49,7 @@ mime_guess = "2.0.4" miniscript = "10.0.0" mp4 = "0.14.0" ord-bitcoincore-rpc = "0.17.1" +ordinals = { version = "0.0.1", path = "crates/ordinals" } redb = "1.5.0" regex = "1.6.0" reqwest = { version = "0.11.23", features = ["blocking", "json"] } @@ -72,7 +73,7 @@ criterion = "0.5.1" executable-path = "1.0.0" pretty_assertions = "1.2.1" reqwest = { version = "0.11.10", features = ["blocking", "brotli", "json"] } -test-bitcoincore-rpc = { path = "test-bitcoincore-rpc" } +test-bitcoincore-rpc = { path = "crates/test-bitcoincore-rpc" } unindent = "0.2.1" [[bench]] diff --git a/crates/ordinals/Cargo.toml b/crates/ordinals/Cargo.toml new file mode 100644 index 0000000000..e38d806380 --- /dev/null +++ b/crates/ordinals/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ordinals" +version = "0.0.1" +edition = "2021" +description = "Library for interoperating with ordinals and inscriptions" +homepage = "https://github.com/ordinals/ord" +repository = "https://github.com/ordinals/ord" +license = "CC0-1.0" +rust-version = "1.67" + +[dependencies] +serde = { version = "1.0.137", features = ["derive"] } +bitcoin = { version = "0.30.1", features = ["rand"] } +thiserror = "1.0.56" + +[dev-dependencies] +serde_json = { version = "1.0.81", features = ["preserve_order"] } diff --git a/crates/ordinals/src/deserialize_from_str.rs b/crates/ordinals/src/deserialize_from_str.rs new file mode 100644 index 0000000000..175c640652 --- /dev/null +++ b/crates/ordinals/src/deserialize_from_str.rs @@ -0,0 +1,44 @@ +use super::*; + +pub struct DeserializeFromStr(pub T); + +impl<'de, T: FromStr> DeserializeFromStr +where + T::Err: Display, +{ + pub fn with(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(DeserializeFromStr::::deserialize(deserializer)?.0) + } +} + +impl<'de, T: FromStr> Deserialize<'de> for DeserializeFromStr +where + T::Err: Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Self( + FromStr::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)?, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_from_str() { + assert_eq!( + serde_json::from_str::>("\"1\"") + .unwrap() + .0, + 1, + ); + } +} diff --git a/crates/ordinals/src/lib.rs b/crates/ordinals/src/lib.rs new file mode 100644 index 0000000000..1899ca3c8b --- /dev/null +++ b/crates/ordinals/src/lib.rs @@ -0,0 +1,23 @@ +//! Types for interoperating with ordinals and inscriptions. + +use { + bitcoin::{ + consensus::{Decodable, Encodable}, + OutPoint, + }, + serde::{Deserialize, Deserializer, Serialize, Serializer}, + std::{ + fmt::{self, Display, Formatter}, + io, + str::FromStr, + }, + thiserror::Error, +}; + +pub use sat_point::SatPoint; + +#[doc(hidden)] +pub use self::deserialize_from_str::DeserializeFromStr; + +mod deserialize_from_str; +mod sat_point; diff --git a/src/sat_point.rs b/crates/ordinals/src/sat_point.rs similarity index 55% rename from src/sat_point.rs rename to crates/ordinals/src/sat_point.rs index 75c034cf82..48c56efe59 100644 --- a/src/sat_point.rs +++ b/crates/ordinals/src/sat_point.rs @@ -1,5 +1,14 @@ -use super::*; - +use {super::*, bitcoin::transaction::ParseOutPointError, std::num::ParseIntError}; + +/// A satpoint identifies the location of a sat in an output. +/// +/// The string representation of a satpoint consists of that of an outpoint, +/// which identifies and output, followed by `:OFFSET`. For example, the string +/// representation of the first sat of the genesis block coinbase output is +/// `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f:0:0`, +/// that of the second sat of the genesis block coinbase output is +/// `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f:0:1`, and +/// so on and so on. #[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, Default)] pub struct SatPoint { pub outpoint: OutPoint, @@ -44,7 +53,7 @@ impl<'de> Deserialize<'de> for SatPoint { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } @@ -52,21 +61,61 @@ impl FromStr for SatPoint { type Err = Error; fn from_str(s: &str) -> Result { - let (outpoint, offset) = s - .rsplit_once(':') - .ok_or_else(|| anyhow!("invalid satpoint: {s}"))?; + let (outpoint, offset) = s.rsplit_once(':').ok_or_else(|| Error::Colon(s.into()))?; Ok(SatPoint { - outpoint: outpoint.parse()?, - offset: offset.parse()?, + outpoint: outpoint + .parse::() + .map_err(|err| Error::Outpoint { + outpoint: outpoint.into(), + err, + })?, + offset: offset.parse::().map_err(|err| Error::Offset { + offset: offset.into(), + err, + })?, }) } } +#[derive(Debug, Error)] +pub enum Error { + #[error("satpoint `{0}` missing colon")] + Colon(String), + #[error("satpoint offset `{offset}` invalid: {err}")] + Offset { offset: String, err: ParseIntError }, + #[error("satpoint outpoint `{outpoint}` invalid: {err}")] + Outpoint { + outpoint: String, + err: ParseOutPointError, + }, +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn error() { + assert_eq!( + "foo".parse::().unwrap_err().to_string(), + "satpoint `foo` missing colon" + ); + + assert_eq!( + "foo:bar".parse::().unwrap_err().to_string(), + "satpoint outpoint `foo` invalid: OutPoint not in : format" + ); + + assert_eq!( + "1111111111111111111111111111111111111111111111111111111111111111:1:bar" + .parse::() + .unwrap_err() + .to_string(), + "satpoint offset `bar` invalid: invalid digit found in string" + ); + } + #[test] fn from_str_ok() { assert_eq!( diff --git a/test-bitcoincore-rpc/Cargo.toml b/crates/test-bitcoincore-rpc/Cargo.toml similarity index 100% rename from test-bitcoincore-rpc/Cargo.toml rename to crates/test-bitcoincore-rpc/Cargo.toml diff --git a/test-bitcoincore-rpc/src/api.rs b/crates/test-bitcoincore-rpc/src/api.rs similarity index 100% rename from test-bitcoincore-rpc/src/api.rs rename to crates/test-bitcoincore-rpc/src/api.rs diff --git a/test-bitcoincore-rpc/src/lib.rs b/crates/test-bitcoincore-rpc/src/lib.rs similarity index 100% rename from test-bitcoincore-rpc/src/lib.rs rename to crates/test-bitcoincore-rpc/src/lib.rs diff --git a/test-bitcoincore-rpc/src/server.rs b/crates/test-bitcoincore-rpc/src/server.rs similarity index 100% rename from test-bitcoincore-rpc/src/server.rs rename to crates/test-bitcoincore-rpc/src/server.rs diff --git a/test-bitcoincore-rpc/src/state.rs b/crates/test-bitcoincore-rpc/src/state.rs similarity index 100% rename from test-bitcoincore-rpc/src/state.rs rename to crates/test-bitcoincore-rpc/src/state.rs diff --git a/src/decimal.rs b/src/decimal.rs index 70449bdab3..051ed907fd 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -99,7 +99,7 @@ impl<'de> Deserialize<'de> for Decimal { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/deserialize_from_str.rs b/src/deserialize_from_str.rs deleted file mode 100644 index f4c537f546..0000000000 --- a/src/deserialize_from_str.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; - -pub(crate) struct DeserializeFromStr(pub(crate) T); - -impl<'de, T: FromStr> Deserialize<'de> for DeserializeFromStr -where - T::Err: Display, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(Self( - FromStr::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)?, - )) - } -} diff --git a/src/inscriptions/inscription_id.rs b/src/inscriptions/inscription_id.rs index 5f3f70e225..684a2f9183 100644 --- a/src/inscriptions/inscription_id.rs +++ b/src/inscriptions/inscription_id.rs @@ -39,7 +39,7 @@ impl<'de> Deserialize<'de> for InscriptionId { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/lib.rs b/src/lib.rs index 5ad0805eae..a2c18bf780 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,6 @@ use { decimal::Decimal, decimal_sat::DecimalSat, degree::Degree, - deserialize_from_str::DeserializeFromStr, epoch::Epoch, height::Height, inscriptions::{media, teleburn, Charm, Media, ParsedEnvelope}, @@ -53,6 +52,7 @@ use { derive_more::{Display, FromStr}, html_escaper::{Escape, Trusted}, lazy_static::lazy_static, + ordinals::{DeserializeFromStr, SatPoint}, regex::Regex, serde::{Deserialize, Deserializer, Serialize, Serializer}, std::{ @@ -90,7 +90,6 @@ pub use self::{ rarity::Rarity, runes::{Edict, Rune, RuneId, Runestone}, sat::Sat, - sat_point::SatPoint, wallet::transaction_builder::{Target, TransactionBuilder}, }; @@ -118,7 +117,6 @@ mod config; mod decimal; mod decimal_sat; mod degree; -mod deserialize_from_str; mod epoch; mod fee_rate; mod height; @@ -131,7 +129,6 @@ pub mod rarity; mod representation; pub mod runes; pub mod sat; -mod sat_point; mod server_config; pub mod subcommand; mod tally; diff --git a/src/object.rs b/src/object.rs index 5ae7c2dc06..611f592934 100644 --- a/src/object.rs +++ b/src/object.rs @@ -67,7 +67,7 @@ impl<'de> Deserialize<'de> for Object { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/outgoing.rs b/src/outgoing.rs index e5fabadecc..63927e3dd8 100644 --- a/src/outgoing.rs +++ b/src/outgoing.rs @@ -94,7 +94,7 @@ impl<'de> Deserialize<'de> for Outgoing { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/rarity.rs b/src/rarity.rs index 65f1eb788f..6700fc47be 100644 --- a/src/rarity.rs +++ b/src/rarity.rs @@ -104,7 +104,7 @@ impl<'de> Deserialize<'de> for Rarity { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/runes/rune.rs b/src/runes/rune.rs index 8321c81680..4f2acbd81f 100644 --- a/src/runes/rune.rs +++ b/src/runes/rune.rs @@ -88,7 +88,7 @@ impl<'de> Deserialize<'de> for Rune { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/runes/rune_id.rs b/src/runes/rune_id.rs index d5a4e3760c..63fd756a99 100644 --- a/src/runes/rune_id.rs +++ b/src/runes/rune_id.rs @@ -58,7 +58,7 @@ impl<'de> Deserialize<'de> for RuneId { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/runes/spaced_rune.rs b/src/runes/spaced_rune.rs index 73e2c7c6f1..d1dedafe80 100644 --- a/src/runes/spaced_rune.rs +++ b/src/runes/spaced_rune.rs @@ -68,7 +68,7 @@ impl<'de> Deserialize<'de> for SpacedRune { where D: Deserializer<'de>, { - Ok(DeserializeFromStr::deserialize(deserializer)?.0) + DeserializeFromStr::with(deserializer) } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 0a499b2702..0d597b3076 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -2,7 +2,6 @@ use { self::{ accept_encoding::AcceptEncoding, accept_json::AcceptJson, - deserialize_from_str::DeserializeFromStr, error::{OptionExt, ServerError, ServerResult}, }, super::*, diff --git a/tests/lib.rs b/tests/lib.rs index 0dd06460ec..7f4cb7fef6 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -20,8 +20,9 @@ use { inscriptions::InscriptionsJson, output::OutputJson, rune::RuneJson, runes::RunesJson, sat::SatJson, status::StatusJson, transaction::TransactionJson, }, - Edict, InscriptionId, Rune, RuneEntry, RuneId, Runestone, SatPoint, + Edict, InscriptionId, Rune, RuneEntry, RuneId, Runestone, }, + ordinals::SatPoint, pretty_assertions::assert_eq as pretty_assert_eq, regex::Regex, reqwest::{StatusCode, Url},