feat: Generic unit on amount #1470

Merged
thesimplekid merged 4 commits from generic_unit_on_amount into main 2026-01-09 18:55:20 +00:00
thesimplekid commented 2025-12-28 23:22:35 +00:00 (Migrated from github.com)

Description


The Cashu protocol supports multiple currency units (Sat, Msat, Usd, Eur, etc.), but the previous Amount type was a simple u64 wrapper with no awareness of units. This created a subtle but dangerous class of bugs: nothing prevented code from accidentally adding 1000 satoshis to 500 millisatoshis, producing a nonsensical result of 1500 in an undefined unit. These bugs are especially insidious because the code compiles and runs without error—the incorrect math only manifests as wrong balances or failed payments in production.

This change makes unit mismatches impossible by encoding the currency unit into the type system. Amount now carries its unit at the type level, and arithmetic operations verify unit compatibility before proceeding. The compiler catches many mistakes statically, and runtime checks catch the rest with a clear UnitMismatch error rather than silent corruption.

The refactor maintains backwards compatibility by using Amount<()> (untyped) at serialization boundaries where the wire protocol expects a plain integer, while internal mint logic now uses Amount to get the safety guarantees. The with_unit() method provides a clear conversion point at the boundary between protocol parsing and application logic, making it explicit where unit context is being added.

This is particularly important for the mint's quote handling, where amounts flow between payment backends (which may use different units internally) and the core mint logic. By making MintQuote.amount_paid, MeltQuote.fee_reserve, and payment response types carry their units, we ensure that fee calculations, balance checks, and unit conversions are always performed correctly.

closes: https://github.com/cashubtc/cdk/issues/1463

Notes to the reviewers


Suggested CHANGELOG Updates

CHANGED

ADDED

REMOVED

FIXED


Checklist

### Description <!-- Describe the purpose of this PR, what's being adding and/or fixed --> ----- The Cashu protocol supports multiple currency units (Sat, Msat, Usd, Eur, etc.), but the previous Amount type was a simple u64 wrapper with no awareness of units. This created a subtle but dangerous class of bugs: nothing prevented code from accidentally adding 1000 satoshis to 500 millisatoshis, producing a nonsensical result of 1500 in an undefined unit. These bugs are especially insidious because the code compiles and runs without error—the incorrect math only manifests as wrong balances or failed payments in production. This change makes unit mismatches impossible by encoding the currency unit into the type system. Amount<CurrencyUnit> now carries its unit at the type level, and arithmetic operations verify unit compatibility before proceeding. The compiler catches many mistakes statically, and runtime checks catch the rest with a clear UnitMismatch error rather than silent corruption. The refactor maintains backwards compatibility by using Amount<()> (untyped) at serialization boundaries where the wire protocol expects a plain integer, while internal mint logic now uses Amount<CurrencyUnit> to get the safety guarantees. The with_unit() method provides a clear conversion point at the boundary between protocol parsing and application logic, making it explicit where unit context is being added. This is particularly important for the mint's quote handling, where amounts flow between payment backends (which may use different units internally) and the core mint logic. By making MintQuote.amount_paid, MeltQuote.fee_reserve, and payment response types carry their units, we ensure that fee calculations, balance checks, and unit conversions are always performed correctly. closes: https://github.com/cashubtc/cdk/issues/1463 - [ ] test with lnbits - [x] wait on https://github.com/cashubtc/cdk/pull/1251 ### Notes to the reviewers <!-- In this section you can include notes directed to the reviewers, like explaining why some parts of the PR were done in a specific way --> ----- ### Suggested [CHANGELOG](https://github.com/cashubtc/cdk/blob/main/CHANGELOG.md) Updates <!-- Please do not edit the actual changelog but note what you changed here. --> #### CHANGED #### ADDED #### REMOVED #### FIXED ---- ### Checklist * [ ] I followed the [code style guidelines](https://github.com/cashubtc/cdk/blob/main/CODE_STYLE.md) * [ ] I ran `just final-check` before committing
davidcaseria (Migrated from github.com) approved these changes 2025-12-28 23:34:50 +00:00
crodas (Migrated from github.com) reviewed 2025-12-29 19:48:36 +00:00
@ -277,0 +447,4 @@
/// # use cashu::{Amount, nuts::CurrencyUnit};
/// let amount = Amount::new(1000, CurrencyUnit::Sat);
/// let (value, unit) = amount.into_parts();
/// assert_eq!(value, 1000);
crodas (Migrated from github.com) commented 2025-12-29 19:48:36 +00:00

@thesimplekid Pardon my ignorance, but wouldn't it be better if the other amount had the same generic, and we refuse to do the math if the currency units are different? Or the implementor can implement a way to unify the units if possible (from Sat to MSat, for instance).

This is a great chance in my opinion, to reduce confusions when doing math with different units

@thesimplekid Pardon my ignorance, but wouldn't it be better if the other amount had the same generic, and we refuse to do the math if the currency units are different? Or the implementor can implement a way to unify the units if possible (from Sat to MSat, for instance). This is a great chance in my opinion, to reduce confusions when doing math with different units
thesimplekid (Migrated from github.com) reviewed 2025-12-29 19:59:43 +00:00
@ -277,0 +447,4 @@
/// # use cashu::{Amount, nuts::CurrencyUnit};
/// let amount = Amount::new(1000, CurrencyUnit::Sat);
/// let (value, unit) = amount.into_parts();
/// assert_eq!(value, 1000);
thesimplekid (Migrated from github.com) commented 2025-12-29 19:59:42 +00:00

If the other Amount had a unit there would be no need for a generic or a second unit type. This allows us to ensure our units match in the mint where it matters without losing copy and having to introduce clone in even more places throughout the wallet making this a much harder refactor.

If the other Amount had a unit there would be no need for a generic or a second unit type. This allows us to ensure our units match in the mint where it matters without losing copy and having to introduce clone in even more places throughout the wallet making this a much harder refactor.
thesimplekid (Migrated from github.com) reviewed 2025-12-29 20:27:21 +00:00
@ -277,0 +447,4 @@
/// # use cashu::{Amount, nuts::CurrencyUnit};
/// let amount = Amount::new(1000, CurrencyUnit::Sat);
/// let (value, unit) = amount.into_parts();
/// assert_eq!(value, 1000);
thesimplekid (Migrated from github.com) commented 2025-12-29 20:27:20 +00:00

Or the implementor can implement a way to unify the units if possible (from Sat to MSat, for instance).

There is already a convert to fn for this.

> Or the implementor can implement a way to unify the units if possible (from Sat to MSat, for instance). There is already a convert to fn for this.
crodas (Migrated from github.com) reviewed 2025-12-29 21:53:56 +00:00
crodas (Migrated from github.com) reviewed 2025-12-30 14:13:52 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cashubtc/cdk!1470
No description provided.