|
| 1 | +# Amount |
| 2 | + |
| 3 | +In this section, we will demonstrate different ways of working with Bitcoin amounts using the `Amount` type. The examples in this section will: |
| 4 | + |
| 5 | +- Justify the MAX 21 million decision |
| 6 | +- Demonstrate parsing and formatting strings |
| 7 | +- Show basic best practices for `NumOpResult` (more complex explanation [here]) |
| 8 | + |
| 9 | +**The 21 Million Bitcoin Limit** |
| 10 | + |
| 11 | +Only 21 million Bitcoins will ever be in circulation. This number is hardcoded in the Bitcoin protocol. |
| 12 | +The logic behind this decision is to create scarcity and protect Bitcoin against inflation as the digital gold. |
| 13 | +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: |
| 14 | + |
| 15 | +50 + 25 + 12.5 + 6.25 + 3.125 + 1.5625 + … + 0.000000001 ≈ 100 (almost) |
| 16 | + |
| 17 | +210,000 blocks × 100 BTC (sum of geometric series) = 21,000,000 BTC |
| 18 | + |
| 19 | +The last Bitcoin should be mined at around year 2140. |
| 20 | + |
| 21 | +Bitcoin further scales down to smaller units like satoshis (sats). |
| 22 | +1 BTC = 100,000,000 sats. |
| 23 | +This makes micro-transactions easy despite the high price of a single coin. |
| 24 | + |
| 25 | +**Setup** |
| 26 | + |
| 27 | +If using `rust-bitcoin`, `Amount` is exported: |
| 28 | +```rust |
| 29 | +use bitcoin::Amount; |
| 30 | +``` |
| 31 | + |
| 32 | +Or use the units crate directly: |
| 33 | +```bash |
| 34 | +cargo add bitcoin-units |
| 35 | +``` |
| 36 | +```rust |
| 37 | +use bitcoin_units::Amount; |
| 38 | +``` |
| 39 | + |
| 40 | +For this example, we are going to need this import: |
| 41 | +```rust |
| 42 | +use bitcoin_units::{amount::Denomination, Amount}; |
| 43 | +``` |
| 44 | +Everything else goes into the main function. |
| 45 | +```rust |
| 46 | +fn main() { |
| 47 | + // The 21 million cap |
| 48 | + let max = Amount::MAX; |
| 49 | + println!("Maximum amount: {} satoshis", max.to_sat()); |
| 50 | + println!("Maximum amount: {}", max.display_in(Denomination::Bitcoin).show_denomination()); |
| 51 | + |
| 52 | + // Exceeding the cap returns an error |
| 53 | + let too_big = Amount::from_sat(Amount::MAX.to_sat() + 1); |
| 54 | + println!("Exceeding MAX: {:?}", too_big); // Err(OutOfRangeError) |
| 55 | + |
| 56 | + // Handling constants - no result handling needed |
| 57 | + let one_btc = Amount::ONE_BTC; |
| 58 | + println!("One BTC = {} satoshis", one_btc.to_sat()); |
| 59 | + |
| 60 | + let zero = Amount::ZERO; |
| 61 | + println!("Zero amount: {} satoshis", zero.to_sat()); |
| 62 | + |
| 63 | + // No result handling for small amounts |
| 64 | + let small = Amount::from_sat_u32(50_000); |
| 65 | + println!("Small Amount = {}", small); |
| 66 | + |
| 67 | + // Result handling for larger amounts |
| 68 | + let large = Amount::from_sat(100_000_000).expect("valid amount"); |
| 69 | + println!("Large Amount = {}", large); |
| 70 | + |
| 71 | + // Parsing string type to Amount - result handling needed for potential error |
| 72 | + let amount1: Amount = "0.1 BTC".parse().expect("valid amount"); |
| 73 | + println!("Amount1 parsed: {}", amount1); |
| 74 | + let amount2 = "100 sat".parse::<Amount>().expect("valid"); |
| 75 | + println!("Amount2 parsed: {}", amount2); |
| 76 | + |
| 77 | + // Formatting with display_in (works without alloc) |
| 78 | + println!("Display in BTC: {}", Amount::ONE_BTC.display_in(Denomination::Bitcoin)); |
| 79 | + println!("Display in Satoshi: {}", Amount::ONE_SAT.display_in(Denomination::Satoshi)); |
| 80 | + println!( |
| 81 | + "Display in BTC with denomination: {}", |
| 82 | + Amount::ONE_BTC.display_in(Denomination::Bitcoin).show_denomination() |
| 83 | + ); |
| 84 | + println!( |
| 85 | + "Display in Satoshi with denomination: {}", |
| 86 | + Amount::ONE_SAT.display_in(Denomination::Satoshi).show_denomination() |
| 87 | + ); |
| 88 | + |
| 89 | + // display_dynamic automatically selects denomination |
| 90 | + println!("Display dynamic: {}", Amount::ONE_SAT.display_dynamic()); // shows in satoshis |
| 91 | + println!("Display dynamic: {}", Amount::ONE_BTC.display_dynamic()); // shows in BTC |
| 92 | + |
| 93 | + // to_string_in and to_string_with_denomination require alloc feature |
| 94 | + #[cfg(feature = "alloc")] |
| 95 | + { |
| 96 | + println!("to_string_in: {}", Amount::ONE_BTC.to_string_in(Denomination::Bitcoin)); |
| 97 | + println!( |
| 98 | + "to_string_with_denomination: {}", |
| 99 | + Amount::ONE_SAT.to_string_with_denomination(Denomination::Satoshi) |
| 100 | + ); |
| 101 | + } |
| 102 | + |
| 103 | + // Arithmetic operations return NumOpResult |
| 104 | + let a = Amount::from_sat(1000).expect("valid"); |
| 105 | + let b = Amount::from_sat(500).expect("valid"); |
| 106 | + |
| 107 | + let sum = a + b; // Returns NumOpResult<Amount> |
| 108 | + println!("Sum = {:?}", sum); |
| 109 | + |
| 110 | + // Extract the value using .unwrap() |
| 111 | + let sum_amount = (a + b).unwrap(); |
| 112 | + println!("Sum amount: {} satoshis", sum_amount.to_sat()); |
| 113 | + |
| 114 | + // Error in case of a negative result |
| 115 | + let tiny = Amount::from_sat(100).expect("valid"); |
| 116 | + let big = Amount::from_sat(1000).expect("valid"); |
| 117 | + let difference = tiny - big; |
| 118 | + println!("Underflow result: {:?}", difference); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +**Creating Amounts** |
| 123 | + |
| 124 | +There are different ways of creating and representing amounts. |
| 125 | +The 21 million cap is represented using the `MAX` constant. |
| 126 | +This constant is used to validate inputs, set logic boundaries, and implement |
| 127 | +sanity checks when testing. It is also more readable compared to hardcoding |
| 128 | +the full 21 million amount. |
| 129 | + |
| 130 | +`from_sat_u32` accepts a `u32`, which is small enough to always be within the |
| 131 | +valid range, so no result handling is needed. `from_sat` accepts a `u64` which |
| 132 | +can exceed the 21 million cap, hence the `Result`. |
| 133 | + |
| 134 | +The `Denomination` enum specifies which unit to display the value in. When you |
| 135 | +call `show_denomination()`, it prints the unit alongside the value. When the |
| 136 | +amount exceeds 21 million, it throws an out-of-range error. Other constants used to represent Bitcoin amounts include `ONE_SAT`, `ONE_BTC`, |
| 137 | +`FIFTY_BTC`, and `ZERO`. |
| 138 | + |
| 139 | +**Parsing and Formatting** |
| 140 | + |
| 141 | +When parsing small amounts, result handling is not strictly necessary unless |
| 142 | +you want extra caution. We use `.expect()` to handle results for larger amounts. |
| 143 | +The `rust-bitcoin` library also allows us to parse amounts as strings and output |
| 144 | +them as `Amount` using the `parse` method. Error handling is necessary in this case. |
| 145 | + |
| 146 | +When formatting output, the preferred method is `.display_in()`. It can be |
| 147 | +combined with `fmt::Formatter` options to precisely control zeros, padding, and |
| 148 | +alignment — similar to how floats work in `core`, except that it's more precise, |
| 149 | +meaning no rounding occurs. It also works without `alloc`, making it suitable for |
| 150 | +`no_std` environments such as hardware wallets or embedded signing devices. |
| 151 | + |
| 152 | +Alternatively, `.display_dynamic()` automatically selects the denomination — |
| 153 | +displaying in BTC for amounts greater than or equal to 1 BTC, and in satoshis |
| 154 | +otherwise. The denomination is always shown to avoid ambiguity. |
| 155 | + |
| 156 | +If you need a `String` directly, `.to_string_in()` outputs just the number and |
| 157 | +`.to_string_with_denomination()` includes the units. These are convenience wrappers |
| 158 | +around `.display_in()` and require the `alloc` feature. |
| 159 | + |
| 160 | +Note that the exact formatting behaviour of `.display_in()` may change between |
| 161 | +versions, though it guarantees accurate human-readable output that round-trips |
| 162 | +with `parse`. |
| 163 | + |
| 164 | +**NumOpResult** |
| 165 | + |
| 166 | +Performing arithmetic operations produces a `NumOpResult`, which we discuss in more detail [here]. |
| 167 | +All you need to understand for now is that arithmetic on amounts does not panic in case of errors — instead it returns `Valid(Amount)` on success and `Error` on failure. |
| 168 | +We therefore explicitly extract the result using `.unwrap()` or other proper error handling options. |
0 commit comments