Skip to content

Commit e1c8a0c

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

3 files changed

Lines changed: 191 additions & 0 deletions

File tree

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Bitcoin is majorly expressed as BTC(bitcoins), mBTC(millibitcoins), sats(satoshis) or bits.
2+
The BTC unit represents a 10^8 value so as to have sub-unit precision instead of large whole numbers.
3+
This allows divisions of 1/10th, 1/100th and so on.
4+
5+
The satoshi, named after the original bitcoin creator, Satoshi Nakamoto, is the smallest unit of bitcoin currency representing a hundred millionth of one Bitcoin (0.00000001 BTC).
6+
7+
The Bitcoin source code uses satoshi to specify any Bitcoin amount and all amounts on the blockchain are denominated in satoshi before they get converted for display.
8+
9+
Amounts can also be represented using satoshis to enhance readability when handling extremely fine bitcoin fractions like when handling fees or faucet rewards.
10+
11+
Beyond Satoshi, payment channels might use even smaller units such as millisatoshis (one hundred billionths of a bitcoin) to represent even more granular amounts.
12+
13+
The `Amount` type represents a non-negative Bitcoin amount, stored internally
14+
as satoshis. For cases where a negative value is needed, `rust-bitcoin` provides
15+
the `SignedAmount` type.
16+
17+
We provide the following examples:
18+
- [Amount](units/amount.md)
19+
- [NumOpResult](units/numopresult.md)
20+
- [Calculating fees](units/fees.md)
21+
- [Lock times](units/locktimes.md)

cookbook/src/units/amount.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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

Comments
 (0)