diff --git a/cookbook/src/SUMMARY.md b/cookbook/src/SUMMARY.md index f857cd5..5ba0688 100644 --- a/cookbook/src/SUMMARY.md +++ b/cookbook/src/SUMMARY.md @@ -9,3 +9,5 @@ - [Working with PSBTs](psbt.md) - [Constructing and Signing Multiple Inputs - SegWit V0](psbt/multiple_inputs_segwit-v0.md) - [Constructing and Signing Multiple Inputs - Taproot](psbt/multiple_inputs_taproot.md) +- [Working with Units](units.md) + - [Amount](units/amount.md) diff --git a/cookbook/src/units.md b/cookbook/src/units.md new file mode 100644 index 0000000..26776fa --- /dev/null +++ b/cookbook/src/units.md @@ -0,0 +1,14 @@ +Bitcoin amounts are usually expressed in BTC or satoshis (sats), +where 1 BTC = 100,000,000 sats. +Beyond the satoshi, payment channels might use even smaller units such as millibitcoins (mBTC) to represent more granular amounts. + +The `Amount` type represents a non-negative bitcoin amount, stored internally +as satoshis — all amounts in `rust-bitcoin` are denominated in satoshi before +they are converted for display. For cases where we need a negative value, +`rust-bitcoin` provides the `SignedAmount` type. + +We provide the following examples: +- [Amount](units/amount.md) +- [NumOpResult](units/numopresult.md) +- [Calculating fees](units/fees.md) +- [Lock times](units/locktimes.md) diff --git a/cookbook/src/units/amount.md b/cookbook/src/units/amount.md new file mode 100644 index 0000000..5cb37a1 --- /dev/null +++ b/cookbook/src/units/amount.md @@ -0,0 +1,162 @@ +# Amount + +In this section, we will demonstrate different ways of working with Bitcoin amounts using the `Amount` type. The examples in this section will: + +- Justify the MAX 21 million decision +- Demonstrate parsing and formatting strings +- Show basic best practices for `NumOpResult` + +**The 21 Million Bitcoin Limit** + +Only 21 million bitcoins will ever be in circulation. This number is hardcoded in the Bitcoin protocol. +The logic behind this decision is to create scarcity and protect Bitcoin against inflation as the digital gold. +Bitcoin is distributed as a result of mining a block and after every 210,000 blocks, the reward is halved and the complexity increases. The first reward was 50 BTC, so: + +50 + 25 + 12.5 + 6.25 + 3.125 + 1.5625 + … + 0.000000001 ≈ 100 (almost) + +210,000 blocks × 100 BTC (sum of geometric series) = 21,000,000 BTC + +The last bitcoin should be mined at around year 2140. + +Bitcoin further scales down to smaller units like satoshis (sats). +1 BTC = 100,000,000 sats. +This makes micro-transactions easy despite the high price of a single coin. + +**Setup** + +If using `rust-bitcoin`, `Amount` is exported: +```rust +use bitcoin::Amount; +``` + +Or use the units crate directly: +```bash +cargo add bitcoin-units +``` +```rust +use bitcoin_units::Amount; +``` + +For this example, we are going to need this import: +```rust +use bitcoin_units::{amount::Denomination, Amount}; +``` +Everything else goes into the main function. +```rust +fn main() { + // The 21 million cap. + let max = Amount::MAX; + println!("Maximum amount: {} satoshis", max.to_sat()); + println!("Maximum amount: {}", max.display_in(Denomination::Bitcoin).show_denomination()); + + // Exceeding the cap returns an error. + let too_big = Amount::from_sat(Amount::MAX.to_sat() + 1); + println!("Exceeding MAX: {:?}", too_big); // `Err(OutOfRangeError)`. + + // Handling constants - no result handling needed. + let one_btc = Amount::ONE_BTC; + println!("One BTC = {} satoshis", one_btc.to_sat()); + + let zero = Amount::ZERO; + println!("Zero amount: {} satoshis", zero.to_sat()); + + // The `Amount` constructor errors if input value is too large. + let large = Amount::from_sat(100_000_000).expect("valid amount"); + println!("Large Amount = {}", large); + + // Error free construction for amounts under approx 42 BTC ([u32::MAX]). + let small = Amount::from_sat_u32(50_000); + println!("Small Amount = {}", small); + + // Parsing string type to `Amount` - result handling needed for potential error. + let amount1: Amount = "0.1 BTC".parse().expect("valid amount"); + println!("Amount1 parsed: {}", amount1); + let amount2 = "100 sat".parse::().expect("valid"); + println!("Amount2 parsed: {}", amount2); + + // Formatting with display_in (works without alloc). + println!("Display in BTC: {}", Amount::ONE_BTC.display_in(Denomination::Bitcoin)); + println!("Display in satoshis: {}", Amount::ONE_SAT.display_in(Denomination::Satoshi)); + println!( + "Display in BTC with denomination: {}", + Amount::ONE_BTC.display_in(Denomination::Bitcoin).show_denomination() + ); + println!( + "Display in satoshis with denomination: {}", + Amount::ONE_SAT.display_in(Denomination::Satoshi).show_denomination() + ); + + // `display_dynamic` automatically selects denomination. + println!("Display dynamic: {}", Amount::ONE_SAT.display_dynamic()); // shows in satoshis. + println!("Display dynamic: {}", Amount::ONE_BTC.display_dynamic()); // shows in BTC. + + // `to_string_in` and `to_string_with_denomination` both require the `alloc` feature. + #[cfg(feature = "alloc")] + { + println!("to_string_in: {}", Amount::ONE_BTC.to_string_in(Denomination::Bitcoin)); + println!( + "to_string_with_denomination: {}", + Amount::ONE_SAT.to_string_with_denomination(Denomination::Satoshi) + ); + } + + // Arithmetic operations return `NumOpResult`. + let a = Amount::from_sat(1000).expect("valid"); + let b = Amount::from_sat(500).expect("valid"); + + let sum = a + b; // Returns `NumOpResult`. + println!("Sum = {:?}", sum); + + // Extract the value using `.unwrap()`. + let sum_amount = (a + b).unwrap(); + println!("Sum amount: {} satoshis", sum_amount.to_sat()); + + // Error in case of a negative result. + let tiny = Amount::from_sat(100).expect("valid"); + let big = Amount::from_sat(1000).expect("valid"); + let difference = tiny - big; + println!("Underflow result: {:?}", difference); +} +``` + +**Creating Amounts** + +There are different ways of creating and representing amounts. +The 21 million cap is represented using the `MAX` constant. +This constant is used to validate inputs, set logic boundaries, and implement +sanity checks when testing. It is also more readable compared to hardcoding +the full 21 million amount. + +`from_sat_u32` accepts a `u32`, which is small enough to always be within the +valid range, so result handling is not necessary. `from_sat` accepts a `u64`, which can exceed the 21 million cap, hence the `Result`. + +The `Denomination` enum specifies which unit to display the value in. When you +call `show_denomination()`, it prints the unit alongside the value. When the +amount exceeds 21 million, it throws an out-of-range error. Other constants used to represent Bitcoin amounts include `ONE_SAT`, `ONE_BTC`, +`FIFTY_BTC`, and `ZERO`. + +**Parsing and Formatting** + +We can parse `Amount` from strings using `parse()` as long as it has a denomination like `"0.1 BTC"` or `"100 sat"` , unless it's zero. +Result handling is always necessary since the input could be invalid. +We use `.expect()` for simplicity in these examples. + +When formatting outputs, the preferred method is `.display_in()`, to output a number. +For a `String` output, we use `.to_string_in()` to output a plain string or +`.to_string_with_denomination()` to include the denomination. +These are convenience wrappers +around `.display_in()` and they require `alloc`. + +Alternatively, `.display_dynamic()` automatically selects the denomination — +displaying in BTC for amounts greater than or equal to 1 BTC, and otherwise in satoshis. + +Note that the exact formatting behaviour of `.display_in()` may change between +versions, though it guarantees accurate human-readable output that round-trips +with `parse`. + +**NumOpResult** + +Performing arithmetic operations produces a `NumOpResult`, which we discuss in more detail [here]. +All we are highlighting here is that arithmetic on `Amount` values does not panic in case of errors — instead, it returns `Valid(Amount)` on success and `Error` on failure. + +We therefore explicitly extract the result using `.unwrap()` or other proper error handling options. \ No newline at end of file