Skip to content

Commit 273d971

Browse files
committed
docs: add amount example for units crate
1 parent a7e819f commit 273d971

File tree

3 files changed

+178
-0
lines changed

3 files changed

+178
-0
lines changed

cookbook/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@
99
- [Working with PSBTs](psbt.md)
1010
- [Constructing and Signing Multiple Inputs - SegWit V0](psbt/multiple_inputs_segwit-v0.md)
1111
- [Constructing and Signing Multiple Inputs - Taproot](psbt/multiple_inputs_taproot.md)
12+
- [Working with Units](units.md)
13+
- [Amount](units/amount.md)

cookbook/src/units.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Bitcoin amounts are usually expressed in BTC or satoshis (sats),
2+
where 1 BTC = 100,000,000 sats.
3+
Beyond the satoshi, payment channels might use even smaller units such as millibitcoins (mBTC) to represent more granular amounts.
4+
5+
The `Amount` type represents a non-negative bitcoin amount, stored internally
6+
as satoshis — all amounts in `rust-bitcoin` are denominated in satoshi before
7+
they are converted for display. For cases where we need a negative value,
8+
`rust-bitcoin` provides the `SignedAmount` type.
9+
10+
We provide the following examples:
11+
- [Amount](units/amount.md)
12+
- [NumOpResult](units/numopresult.md)
13+
- [Calculating fees](units/fees.md)
14+
- [Lock times](units/locktimes.md)

cookbook/src/units/amount.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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`
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+
// The `Amount` constructor errors if input value is too large.
64+
let large = Amount::from_sat(100_000_000).expect("valid amount");
65+
println!("Large Amount = {}", large);
66+
67+
// Error free construction for amounts under approx 42 BTC ([u32::MAX]).
68+
let small = Amount::from_sat_u32(50_000);
69+
println!("Small Amount = {}", small);
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 satoshis: {}", 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 satoshis 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` both require the `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 result handling is not necessary. `from_sat` accepts a `u64`, which can exceed the 21 million cap, hence the `Result`.
132+
133+
The `Denomination` enum specifies which unit to display the value in. When you
134+
call `show_denomination()`, it prints the unit alongside the value. When the
135+
amount exceeds 21 million, it throws an out-of-range error. Other constants used to represent Bitcoin amounts include `ONE_SAT`, `ONE_BTC`,
136+
`FIFTY_BTC`, and `ZERO`.
137+
138+
**Parsing and Formatting**
139+
140+
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.
141+
Result handling is always necessary since the input could be invalid.
142+
We use `.expect()` for simplicity in these examples.
143+
144+
When formatting outputs, the preferred method is `.display_in()`, to output a number.
145+
For a `String` output, we use `.to_string_in()` to output a plain string or
146+
`.to_string_with_denomination()` to include the denomination.
147+
These are convenience wrappers
148+
around `.display_in()` and they require `alloc`.
149+
150+
Alternatively, `.display_dynamic()` automatically selects the denomination —
151+
displaying in BTC for amounts greater than or equal to 1 BTC, and otherwise in satoshis.
152+
153+
Note that the exact formatting behaviour of `.display_in()` may change between
154+
versions, though it guarantees accurate human-readable output that round-trips
155+
with `parse`.
156+
157+
**NumOpResult**
158+
159+
Performing arithmetic operations produces a `NumOpResult`, which we discuss in more detail [here].
160+
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.
161+
162+
We therefore explicitly extract the result using `.unwrap()` or other proper error handling options.

0 commit comments

Comments
 (0)