Add bitcoin-units crate

Add a new crate `bitcoin-units`, move the `amount` module over to it and
re-export all types from `bitcoin::amount` so this as not a breaking
change.
This commit is contained in:
Tobin C. Harding 2023-08-08 18:47:05 +10:00
parent 4ecb1fe7da
commit 69e56a64ed
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
18 changed files with 258 additions and 41 deletions

View File

@ -17,7 +17,6 @@ jobs:
matrix: matrix:
fuzz_target: [ fuzz_target: [
bitcoin_deserialize_address, bitcoin_deserialize_address,
bitcoin_deserialize_amount,
bitcoin_deserialize_block, bitcoin_deserialize_block,
bitcoin_deserialize_prefilled_transaction, bitcoin_deserialize_prefilled_transaction,
bitcoin_deserialize_psbt, bitcoin_deserialize_psbt,
@ -34,6 +33,7 @@ jobs:
hashes_sha256, hashes_sha256,
hashes_sha512_256, hashes_sha512_256,
hashes_sha512, hashes_sha512,
units_deserialize_amount,
] ]
steps: steps:
- name: Install test dependencies - name: Install test dependencies

View File

@ -39,6 +39,7 @@ dependencies = [
"bincode", "bincode",
"bitcoin-internals", "bitcoin-internals",
"bitcoin-io", "bitcoin-io",
"bitcoin-units",
"bitcoin_hashes", "bitcoin_hashes",
"bitcoinconsensus", "bitcoinconsensus",
"hex-conservative", "hex-conservative",
@ -72,6 +73,15 @@ dependencies = [
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "bitcoin-units"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"serde_test",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
version = "0.13.0" version = "0.13.0"

View File

@ -38,6 +38,7 @@ dependencies = [
"bincode", "bincode",
"bitcoin-internals", "bitcoin-internals",
"bitcoin-io", "bitcoin-io",
"bitcoin-units",
"bitcoin_hashes", "bitcoin_hashes",
"bitcoinconsensus", "bitcoinconsensus",
"hex-conservative", "hex-conservative",
@ -71,6 +72,15 @@ dependencies = [
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "bitcoin-units"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"serde_test",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
version = "0.13.0" version = "0.13.0"

View File

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["bitcoin", "hashes", "internals", "fuzz", "io"] members = ["bitcoin", "hashes", "internals", "fuzz", "io", "units"]
resolver = "2" resolver = "2"
[patch.crates-io.bitcoin] [patch.crates-io.bitcoin]
@ -13,3 +13,6 @@ path = "internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "io" path = "io"
[patch.crates-io.bitcoin-units]
path = "units"

View File

@ -15,10 +15,10 @@ exclude = ["tests", "contrib"]
[features] [features]
default = [ "std", "secp-recovery" ] 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-std = ["secp256k1/rand-std", "std"]
rand = ["secp256k1/rand"] 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-lowmemory = ["secp256k1/lowmemory"]
secp-recovery = ["secp256k1/recovery"] secp-recovery = ["secp256k1/recovery"]
bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]
@ -35,6 +35,7 @@ hex_lit = "0.1.1"
internals = { package = "bitcoin-internals", version = "0.2.0" } internals = { package = "bitcoin-internals", version = "0.2.0" }
io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["alloc"] } io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["alloc"] }
secp256k1 = { version = "0.28.0", default-features = false, features = ["hashes", "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 } base64 = { version = "0.21.3", optional = true }
# Only use this feature for no-std builds, otherwise use bitcoinconsensus-std. # Only use this feature for no-std builds, otherwise use bitcoinconsensus-std.

View File

@ -35,3 +35,6 @@ path = "../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../io" path = "../../io"
[patch.crates-io.bitcoin-units]
path = "../../units"

View File

@ -91,7 +91,6 @@ mod serde_utils;
#[macro_use] #[macro_use]
pub mod p2p; pub mod p2p;
pub mod address; pub mod address;
pub mod amount;
pub mod base58; pub mod base58;
pub mod bip152; pub mod bip152;
pub mod bip158; pub mod bip158;
@ -187,3 +186,35 @@ mod prelude {
pub use hex::DisplayHex; 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: io::Read + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
Ok(Amount::from_sat(Decodable::consensus_decode(r)?))
}
}
impl Encodable for Amount {
#[inline]
fn consensus_encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.to_sat().consensus_encode(w)
}
}
}

View File

@ -2,7 +2,7 @@
set -ex set -ex
CRATES="bitcoin hashes internals fuzz" CRATES="bitcoin hashes units internals fuzz"
DEPS="recent minimal" DEPS="recent minimal"
for dep in $DEPS for dep in $DEPS

View File

@ -21,10 +21,6 @@ serde_cbor = "0.9"
name = "bitcoin_deserialize_address" name = "bitcoin_deserialize_address"
path = "fuzz_targets/bitcoin/deserialize_address.rs" path = "fuzz_targets/bitcoin/deserialize_address.rs"
[[bin]]
name = "bitcoin_deserialize_amount"
path = "fuzz_targets/bitcoin/deserialize_amount.rs"
[[bin]] [[bin]]
name = "bitcoin_deserialize_block" name = "bitcoin_deserialize_block"
path = "fuzz_targets/bitcoin/deserialize_block.rs" path = "fuzz_targets/bitcoin/deserialize_block.rs"
@ -88,3 +84,7 @@ path = "fuzz_targets/hashes/sha512_256.rs"
[[bin]] [[bin]]
name = "hashes_sha512" name = "hashes_sha512"
path = "fuzz_targets/hashes/sha512.rs" path = "fuzz_targets/hashes/sha512.rs"
[[bin]]
name = "units_deserialize_amount"
path = "fuzz_targets/units/deserialize_amount.rs"

View File

@ -36,3 +36,6 @@ path = "../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../io" path = "../../io"
[patch.crates-io.bitcoin-units]
path = "../../units"

View File

@ -24,3 +24,6 @@ path = "../../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../../io" path = "../../../io"
[patch.crates-io.bitcoin-units]
path = "../../../units"

View File

@ -36,7 +36,7 @@ macro_rules! parse_error_type {
impl core::fmt::Display for $name { impl core::fmt::Display for $name {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 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)
} }
} }

30
units/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "bitcoin-units"
version = "0.1.0"
authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
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"

15
units/README.md Normal file
View File

@ -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/).

79
units/contrib/test.sh Executable file
View File

@ -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

View File

@ -4,22 +4,24 @@
//! //!
//! This module mainly introduces the [Amount] and [SignedAmount] types. //! This module mainly introduces the [Amount] and [SignedAmount] types.
//! We refer to the documentation on the types for more information. //! We refer to the documentation on the types for more information.
//!
use core::cmp::Ordering; use core::cmp::Ordering;
use core::fmt::{self, Write}; use core::fmt::{self, Write};
use core::str::FromStr; use core::str::FromStr;
use core::{default, ops}; use core::{default, ops};
use crate::consensus::encode::{self, Decodable, Encodable}; #[cfg(feature = "serde")]
use crate::prelude::*; use ::serde::{Deserialize, Serialize};
#[cfg(feature = "alloc")]
use crate::prelude::{String, ToString};
/// A set of denominations in which amounts can be expressed. /// A set of denominations in which amounts can be expressed.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use core::str::FromStr; /// # 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 BTC").unwrap(), Amount::from_sat(100_000_000));
/// assert_eq!(Amount::from_str("1 cBTC").unwrap(), Amount::from_sat(1_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::*; use self::ParseAmountError::*;
if CONFUSING_FORMS.contains(&s) { if CONFUSING_FORMS.contains(&s) {
return Err(PossiblyConfusingDenomination(s.to_owned())); return Err(PossiblyConfusingDenomination(s.to_string()));
}; };
let form = self::Denomination::forms(s); 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)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
pub struct Amount(u64); pub struct Amount(u64);
impl Amount { impl Amount {
@ -582,7 +583,7 @@ impl Amount {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use bitcoin::{Amount, Denomination}; /// # use bitcoin_units::amount::{Amount, Denomination};
/// let amount = Amount::from_sat(100_000); /// let amount = Amount::from_sat(100_000);
/// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin)) /// 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: io::Read + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
Ok(Amount(Decodable::consensus_decode(r)?))
}
}
impl Encodable for Amount {
#[inline]
fn consensus_encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.0.consensus_encode(w)
}
}
impl default::Default for Amount { impl default::Default for Amount {
fn default() -> Self { Amount::ZERO } fn default() -> Self { Amount::ZERO }
} }
@ -1215,12 +1202,11 @@ pub mod serde {
//! //!
//! ```rust,ignore //! ```rust,ignore
//! use serde::{Serialize, Deserialize}; //! use serde::{Serialize, Deserialize};
//! use bitcoin::Amount; //! use bitcoin_units::Amount;
//! //!
//! #[derive(Serialize, Deserialize)] //! #[derive(Serialize, Deserialize)]
//! # #[serde(crate = "actual_serde")]
//! pub struct HasAmount { //! pub struct HasAmount {
//! #[serde(with = "bitcoin::amount::serde::as_btc")] //! #[serde(with = "bitcoin_units::amount::serde::as_btc")]
//! pub amount: Amount, //! pub amount: Amount,
//! } //! }
//! ``` //! ```
@ -1999,7 +1985,7 @@ mod tests {
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); 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("-1 BTC", Err(E::Negative));
case("-0.0 BTC", Err(E::Negative)); case("-0.0 BTC", Err(E::Negative));
@ -2135,7 +2121,6 @@ mod tests {
#[test] #[test]
fn serde_as_sat() { fn serde_as_sat() {
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(with = "crate::amount::serde::as_sat")] #[serde(with = "crate::amount::serde::as_sat")]
pub amt: Amount, pub amt: Amount,
@ -2163,7 +2148,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(with = "crate::amount::serde::as_btc")] #[serde(with = "crate::amount::serde::as_btc")]
pub amt: Amount, pub amt: Amount,
@ -2199,7 +2183,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(default, with = "crate::amount::serde::as_btc::opt")] #[serde(default, with = "crate::amount::serde::as_btc::opt")]
pub amt: Option<Amount>, pub amt: Option<Amount>,
@ -2241,7 +2224,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(default, with = "crate::amount::serde::as_sat::opt")] #[serde(default, with = "crate::amount::serde::as_sat::opt")]
pub amt: Option<Amount>, pub amt: Option<Amount>,

47
units/src/lib.rs Normal file
View File

@ -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};
}