Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cookbook/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
14 changes: 14 additions & 0 deletions cookbook/src/units.md
Original file line number Diff line number Diff line change
@@ -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)
162 changes: 162 additions & 0 deletions cookbook/src/units/amount.md
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should try to use little 'b' for the currency and big 'B' for the network.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point


- 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::<Amount>().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<Amount>`.
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.