diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 1e7f58f2..865f7c92 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -17,7 +17,6 @@ jobs: matrix: fuzz_target: [ bitcoin_deserialize_address, - bitcoin_deserialize_amount, bitcoin_deserialize_block, bitcoin_deserialize_prefilled_transaction, bitcoin_deserialize_psbt, @@ -34,6 +33,7 @@ jobs: hashes_sha256, hashes_sha512_256, hashes_sha512, + units_deserialize_amount, ] steps: - name: Install test dependencies diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index c8fcbe8f..b7d2ba73 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -39,6 +39,7 @@ dependencies = [ "bincode", "bitcoin-internals", "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", "bitcoinconsensus", "hex-conservative", @@ -72,6 +73,15 @@ dependencies = [ name = "bitcoin-io" version = "0.1.0" +[[package]] +name = "bitcoin-units" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "serde_test", +] + [[package]] name = "bitcoin_hashes" version = "0.13.0" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index c6af81cf..aa36d727 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -38,6 +38,7 @@ dependencies = [ "bincode", "bitcoin-internals", "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", "bitcoinconsensus", "hex-conservative", @@ -71,6 +72,15 @@ dependencies = [ name = "bitcoin-io" version = "0.1.0" +[[package]] +name = "bitcoin-units" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "serde_test", +] + [[package]] name = "bitcoin_hashes" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index dda25c25..77469170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bitcoin", "hashes", "internals", "fuzz", "io"] +members = ["bitcoin", "hashes", "internals", "fuzz", "io", "units"] resolver = "2" [patch.crates-io.bitcoin] @@ -13,3 +13,6 @@ path = "internals" [patch.crates-io.bitcoin-io] path = "io" + +[patch.crates-io.bitcoin-units] +path = "units" diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 92341af9..f06cc572 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -15,10 +15,10 @@ exclude = ["tests", "contrib"] [features] default = [ "std", "secp-recovery" ] -std = ["bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "secp256k1/std"] +std = ["bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "secp256k1/std", "units/std"] rand-std = ["secp256k1/rand-std", "std"] rand = ["secp256k1/rand"] -serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde"] +serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde", "units/serde"] secp-lowmemory = ["secp256k1/lowmemory"] secp-recovery = ["secp256k1/recovery"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] @@ -35,6 +35,7 @@ hex_lit = "0.1.1" internals = { package = "bitcoin-internals", version = "0.2.0" } io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["alloc"] } secp256k1 = { version = "0.28.0", default-features = false, features = ["hashes", "alloc"] } +units = { package = "bitcoin-units", version = "0.1.0", default-features = false, features = ["alloc"] } base64 = { version = "0.21.3", optional = true } # Only use this feature for no-std builds, otherwise use bitcoinconsensus-std. diff --git a/bitcoin/embedded/Cargo.toml b/bitcoin/embedded/Cargo.toml index 0442c8fc..3f26fce5 100644 --- a/bitcoin/embedded/Cargo.toml +++ b/bitcoin/embedded/Cargo.toml @@ -35,3 +35,6 @@ path = "../../internals" [patch.crates-io.bitcoin-io] path = "../../io" + +[patch.crates-io.bitcoin-units] +path = "../../units" diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 1dbd6a58..897f8119 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -91,7 +91,6 @@ mod serde_utils; #[macro_use] pub mod p2p; pub mod address; -pub mod amount; pub mod base58; pub mod bip152; pub mod bip158; @@ -187,3 +186,35 @@ mod prelude { pub use hex::DisplayHex; } + +pub mod amount { + //! Bitcoin amounts. + //! + //! This module mainly introduces the [Amount] and [SignedAmount] types. + //! We refer to the documentation on the types for more information. + + use crate::consensus::{encode, Decodable, Encodable}; + use crate::io; + + #[rustfmt::skip] // Keep public re-exports separate. + #[doc(inline)] + pub use units::amount::{ + Amount, CheckedSum, Denomination, Display, ParseAmountError, SignedAmount, + }; + #[cfg(feature = "serde")] + pub use units::amount::serde; + + impl Decodable for Amount { + #[inline] + fn consensus_decode(r: &mut R) -> Result { + Ok(Amount::from_sat(Decodable::consensus_decode(r)?)) + } + } + + impl Encodable for Amount { + #[inline] + fn consensus_encode(&self, w: &mut W) -> Result { + self.to_sat().consensus_encode(w) + } + } +} diff --git a/contrib/test.sh b/contrib/test.sh index 1e40fb1e..243c3927 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -2,7 +2,7 @@ set -ex -CRATES="bitcoin hashes internals fuzz" +CRATES="bitcoin hashes units internals fuzz" DEPS="recent minimal" for dep in $DEPS diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9abf5cd6..8d7dd702 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -21,10 +21,6 @@ serde_cbor = "0.9" name = "bitcoin_deserialize_address" path = "fuzz_targets/bitcoin/deserialize_address.rs" -[[bin]] -name = "bitcoin_deserialize_amount" -path = "fuzz_targets/bitcoin/deserialize_amount.rs" - [[bin]] name = "bitcoin_deserialize_block" path = "fuzz_targets/bitcoin/deserialize_block.rs" @@ -88,3 +84,7 @@ path = "fuzz_targets/hashes/sha512_256.rs" [[bin]] name = "hashes_sha512" path = "fuzz_targets/hashes/sha512.rs" + +[[bin]] +name = "units_deserialize_amount" +path = "fuzz_targets/units/deserialize_amount.rs" diff --git a/fuzz/fuzz_targets/bitcoin/deserialize_amount.rs b/fuzz/fuzz_targets/units/deserialize_amount.rs similarity index 100% rename from fuzz/fuzz_targets/bitcoin/deserialize_amount.rs rename to fuzz/fuzz_targets/units/deserialize_amount.rs diff --git a/hashes/embedded/Cargo.toml b/hashes/embedded/Cargo.toml index bd3e388d..0aa39f0a 100644 --- a/hashes/embedded/Cargo.toml +++ b/hashes/embedded/Cargo.toml @@ -36,3 +36,6 @@ path = "../../internals" [patch.crates-io.bitcoin-io] path = "../../io" + +[patch.crates-io.bitcoin-units] +path = "../../units" diff --git a/hashes/extended_tests/schemars/Cargo.toml b/hashes/extended_tests/schemars/Cargo.toml index d25cec04..8d262dff 100644 --- a/hashes/extended_tests/schemars/Cargo.toml +++ b/hashes/extended_tests/schemars/Cargo.toml @@ -24,3 +24,6 @@ path = "../../../internals" [patch.crates-io.bitcoin-io] path = "../../../io" + +[patch.crates-io.bitcoin-units] +path = "../../../units" diff --git a/internals/src/error/parse_error.rs b/internals/src/error/parse_error.rs index 7bcf8ceb..d8a99d21 100644 --- a/internals/src/error/parse_error.rs +++ b/internals/src/error/parse_error.rs @@ -36,7 +36,7 @@ macro_rules! parse_error_type { impl core::fmt::Display for $name { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - $crate::error::write_err!("{}", self.input.display_cannot_parse($subject); self.source) + $crate::error::write_err!(f, "{}", self.input.display_cannot_parse($subject); self.source) } } diff --git a/units/Cargo.toml b/units/Cargo.toml new file mode 100644 index 00000000..8b8a49d8 --- /dev/null +++ b/units/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "bitcoin-units" +version = "0.1.0" +authors = ["Andrew Poelstra "] +license = "CC0-1.0" +repository = "https://github.com/rust-bitcoin/rust-bitcoin/" +description = "Basic Bitcoin numeric units such as amount" +categories = ["cryptography::cryptocurrencies"] +keywords = ["bitcoin", "newtypes"] +readme = "README.md" +edition = "2021" +rust-version = "1.56.1" +exclude = ["tests", "contrib"] + +[features] +default = ["std"] +std = ["alloc"] +alloc = [] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] + +serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +serde_test = "1.0" +serde_json = "1.0" diff --git a/units/README.md b/units/README.md new file mode 100644 index 00000000..4b419c4f --- /dev/null +++ b/units/README.md @@ -0,0 +1,15 @@ +Bitcoin Units +============= + +This crate provides basic Bitcoin numeric units such as `Amount`. + + +## Minimum Supported Rust Version (MSRV) + +This library should always compile with any combination of features on **Rust 1.56.1**. + + +## Licensing + +The code in this project is licensed under the [Creative Commons CC0 1.0 Universal license](LICENSE). +We use the [SPDX license list](https://spdx.org/licenses/) and [SPDX IDs](https://spdx.dev/ids/). diff --git a/units/contrib/test.sh b/units/contrib/test.sh new file mode 100755 index 00000000..e23decf7 --- /dev/null +++ b/units/contrib/test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +set -ex + +FEATURES="std alloc serde" + +cargo --version +rustc --version + +# Work out if we are using a nightly toolchain. +NIGHTLY=false +if cargo --version | grep nightly >/dev/null; then + NIGHTLY=true +fi + +# Make all cargo invocations verbose +export CARGO_TERM_VERBOSE=true + +# Defaults / sanity checks +cargo build +cargo test + +if [ "$DO_LINT" = true ] +then + cargo clippy --locked --all-features --all-targets -- -D warnings +fi + +if [ "$DO_FEATURE_MATRIX" = true ]; then + # No features + cargo build --locked --no-default-features + cargo test --locked --no-default-features + + # Default features (this is std and alloc) + cargo build --locked + cargo test --locked + + # All features + cargo build --locked --no-default-features --all-features + cargo test --locked --no-default-features --all-features +fi + +REPO_DIR=$(git rev-parse --show-toplevel) + +# Build the docs if told to (this only works with the nightly toolchain) +if [ "$DO_DOCSRS" = true ]; then + RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features +fi + +# Build the docs with a stable toolchain, in unison with the DO_DOCSRS command +# above this checks that we feature guarded docs imports correctly. +if [ "$DO_DOCS" = true ]; then + RUSTDOCFLAGS="-D warnings" cargo +stable doc --all-features +fi + +# Run formatter if told to. +if [ "$DO_FMT" = true ]; then + if [ "$NIGHTLY" = false ]; then + echo "DO_FMT requires a nightly toolchain (consider using RUSTUP_TOOLCHAIN)" + exit 1 + fi + rustup component add rustfmt + cargo fmt --check +fi + +# Bench if told to, only works with non-stable toolchain (nightly, beta). +if [ "$DO_BENCH" = true ] +then + if [ "$NIGHTLY" = false ] + then + if [ -n "$RUSTUP_TOOLCHAIN" ] + then + echo "RUSTUP_TOOLCHAIN is set to a non-nightly toolchain but DO_BENCH requires a nightly toolchain" + else + echo "DO_BENCH requires a nightly toolchain" + fi + exit 1 + fi + RUSTFLAGS='--cfg=bench' cargo bench +fi diff --git a/bitcoin/src/amount.rs b/units/src/amount.rs similarity index 98% rename from bitcoin/src/amount.rs rename to units/src/amount.rs index 496245ed..14c27283 100644 --- a/bitcoin/src/amount.rs +++ b/units/src/amount.rs @@ -4,22 +4,24 @@ //! //! This module mainly introduces the [Amount] and [SignedAmount] types. //! We refer to the documentation on the types for more information. -//! use core::cmp::Ordering; use core::fmt::{self, Write}; use core::str::FromStr; use core::{default, ops}; -use crate::consensus::encode::{self, Decodable, Encodable}; -use crate::prelude::*; +#[cfg(feature = "serde")] +use ::serde::{Deserialize, Serialize}; + +#[cfg(feature = "alloc")] +use crate::prelude::{String, ToString}; /// A set of denominations in which amounts can be expressed. /// /// # Examples /// ``` /// # use core::str::FromStr; -/// # use bitcoin::Amount; +/// # use bitcoin_units::Amount; /// /// assert_eq!(Amount::from_str("1 BTC").unwrap(), Amount::from_sat(100_000_000)); /// assert_eq!(Amount::from_str("1 cBTC").unwrap(), Amount::from_sat(1_000_000)); @@ -126,12 +128,12 @@ impl FromStr for Denomination { use self::ParseAmountError::*; if CONFUSING_FORMS.contains(&s) { - return Err(PossiblyConfusingDenomination(s.to_owned())); + return Err(PossiblyConfusingDenomination(s.to_string())); }; let form = self::Denomination::forms(s); - form.ok_or_else(|| UnknownDenomination(s.to_owned())) + form.ok_or_else(|| UnknownDenomination(s.to_string())) } } @@ -493,7 +495,6 @@ fn fmt_satoshi_in( /// #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] pub struct Amount(u64); impl Amount { @@ -582,7 +583,7 @@ impl Amount { /// /// # Examples /// ``` - /// # use bitcoin::{Amount, Denomination}; + /// # use bitcoin_units::amount::{Amount, Denomination}; /// let amount = Amount::from_sat(100_000); /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin)) /// ``` @@ -687,20 +688,6 @@ impl Amount { } } -impl Decodable for Amount { - #[inline] - fn consensus_decode(r: &mut R) -> Result { - Ok(Amount(Decodable::consensus_decode(r)?)) - } -} - -impl Encodable for Amount { - #[inline] - fn consensus_encode(&self, w: &mut W) -> Result { - self.0.consensus_encode(w) - } -} - impl default::Default for Amount { fn default() -> Self { Amount::ZERO } } @@ -1215,12 +1202,11 @@ pub mod serde { //! //! ```rust,ignore //! use serde::{Serialize, Deserialize}; - //! use bitcoin::Amount; + //! use bitcoin_units::Amount; //! //! #[derive(Serialize, Deserialize)] - //! # #[serde(crate = "actual_serde")] //! pub struct HasAmount { - //! #[serde(with = "bitcoin::amount::serde::as_btc")] + //! #[serde(with = "bitcoin_units::amount::serde::as_btc")] //! pub amount: Amount, //! } //! ``` @@ -1999,7 +1985,7 @@ mod tests { assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); } - case("5 BCH", Err(E::UnknownDenomination("BCH".to_owned()))); + case("5 BCH", Err(E::UnknownDenomination("BCH".to_string()))); case("-1 BTC", Err(E::Negative)); case("-0.0 BTC", Err(E::Negative)); @@ -2135,7 +2121,6 @@ mod tests { #[test] fn serde_as_sat() { #[derive(Serialize, Deserialize, PartialEq, Debug)] - #[serde(crate = "actual_serde")] struct T { #[serde(with = "crate::amount::serde::as_sat")] pub amt: Amount, @@ -2163,7 +2148,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug)] - #[serde(crate = "actual_serde")] struct T { #[serde(with = "crate::amount::serde::as_btc")] pub amt: Amount, @@ -2199,7 +2183,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] - #[serde(crate = "actual_serde")] struct T { #[serde(default, with = "crate::amount::serde::as_btc::opt")] pub amt: Option, @@ -2241,7 +2224,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] - #[serde(crate = "actual_serde")] struct T { #[serde(default, with = "crate::amount::serde::as_sat::opt")] pub amt: Option, diff --git a/units/src/lib.rs b/units/src/lib.rs new file mode 100644 index 00000000..b3afb43a --- /dev/null +++ b/units/src/lib.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Rust Bitcoin units library +//! +//! This library provides basic types used by the Rust Bitcoin ecosystem. + +#![cfg_attr(all(not(test), not(feature = "std")), no_std)] +// Experimental features we need. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Coding conventions +#![warn(missing_docs)] +// Exclude clippy lints we don't think are valuable +#![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134 + +// Disable 16-bit support at least for now as we can't guarantee it yet. +#[cfg(target_pointer_width = "16")] +compile_error!( + "rust-bitcoin currently only supports architectures with pointers wider than 16 bits, let us + know if you want 16-bit support. Note that we do NOT guarantee that we will implement it!" +); + +#[cfg(all(feature = "alloc", not(feature = "std")))] +extern crate alloc; + +#[cfg(not(feature = "std"))] +extern crate core; + +/// A generic serialization/deserialization framework. +#[cfg(feature = "serde")] +pub extern crate serde; + +// TODO: Make amount module less dependent on an allocator. +#[cfg(feature = "alloc")] +pub mod amount; + +#[cfg(feature = "alloc")] +#[doc(inline)] +pub use self::amount::{Amount, ParseAmountError, SignedAmount}; + +#[rustfmt::skip] +mod prelude { + #[cfg(all(feature = "alloc", not(feature = "std"), not(test)))] + pub use alloc::{string::{String, ToString}, borrow::ToOwned}; + + #[cfg(any(feature = "std", test))] + pub use std::{string::{String, ToString}, borrow::ToOwned}; +}