Skip to main content

nautilus_model/defi/types/
money.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! DeFi-specific extensions for the [`Money`] type.
17
18use alloy_primitives::U256;
19use nautilus_core::correctness::{CorrectnessError, CorrectnessResult};
20
21use crate::types::{Currency, Money, Quantity};
22
23impl Money {
24    /// Creates a new [`Money`] instance from an unsigned integer amount at the currency precision.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the precision is invalid or the scaled amount exceeds the signed money
29    /// range.
30    pub fn from_u256(amount: U256, currency: Currency) -> CorrectnessResult<Self> {
31        let quantity = Quantity::from_u256(amount, currency.precision)?;
32        let raw =
33            i128::try_from(quantity.raw()).map_err(|_| CorrectnessError::PredicateViolation {
34                message: format!("Amount for {currency} exceeds Money raw range"),
35            })?;
36        Self::from_raw_checked(raw, currency)
37    }
38
39    /// Creates a new [`Money`] instance from raw wei value with 18-decimal precision.
40    ///
41    /// This method is specifically designed for DeFi applications where values are
42    /// represented in wei (the smallest unit of Ether, 1 ETH = 10^18 wei).
43    ///
44    /// # Panics
45    ///
46    /// Panics if `currency.precision` is not 18, or if the raw wei value exceeds the
47    /// signed 128-bit range.
48    pub fn from_wei<U>(raw_wei: U, currency: Currency) -> Self
49    where
50        U: Into<U256>,
51    {
52        assert!(
53            currency.precision == 18,
54            "`from_wei` requires a currency with precision 18, was {} for {}",
55            currency.precision,
56            currency.code,
57        );
58
59        let raw_u256: U256 = raw_wei.into();
60        let raw_u128: u128 = raw_u256
61            .try_into()
62            .expect("raw wei value exceeds 128-bit range");
63
64        assert!(
65            raw_u128 <= i128::MAX as u128,
66            "raw wei value exceeds signed 128-bit range"
67        );
68
69        let raw_i128: i128 = raw_u128 as i128;
70        Self::from_raw(raw_i128, currency)
71    }
72
73    /// Converts this [`Money`] instance to raw wei value.
74    ///
75    /// # Panics
76    ///
77    /// Panics if `self.currency.precision` is not 18 or `self.raw` is negative.
78    /// For other precisions convert to precision 18 first.
79    #[must_use]
80    pub fn to_wei(&self) -> U256 {
81        assert!(
82            self.currency.precision == 18,
83            "Failed to convert money with precision {} to wei (requires precision 18)",
84            self.currency.precision,
85        );
86        assert!(
87            !self.is_negative(),
88            "Failed to convert negative money to wei"
89        );
90        U256::from(self.raw() as u128)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use alloy_primitives::U256;
97    use rstest::rstest;
98    use rust_decimal::Decimal;
99    use rust_decimal_macros::dec;
100
101    use super::*;
102    use crate::enums::CurrencyType;
103
104    #[rstest]
105    fn test_from_wei_one_eth() {
106        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
107        let one_eth_wei = U256::from(1_000_000_000_000_000_000_u64);
108        let money = Money::from_wei(one_eth_wei, eth);
109
110        // Use decimal comparison for high precision values
111        assert_eq!(money.as_decimal(), dec!(1));
112        assert_eq!(money.currency.precision, 18);
113    }
114
115    #[rstest]
116    fn test_from_u256_scales_six_decimal_currency_exactly() {
117        let usdc = Currency::new("USDC", 6, 0, "USD Coin", CurrencyType::Crypto);
118
119        let money = Money::from_u256(U256::from(987_654_321_u64), usdc).unwrap();
120
121        assert_eq!(money.raw(), 9_876_543_210_000_000_000);
122        assert_eq!(money.as_decimal(), dec!(987.654321));
123        assert_eq!(money.currency, usdc);
124    }
125
126    #[rstest]
127    fn test_from_wei_small_amount() {
128        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
129        let small_wei = U256::from(1_000_000_000_000_u64); // 0.000001 ETH
130        let money = Money::from_wei(small_wei, eth);
131
132        // Use decimal comparison for high precision values
133        assert_eq!(money.as_decimal(), dec!(0.000001)); // 0.000001
134    }
135
136    #[rstest]
137    fn test_to_wei_one_eth() {
138        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
139        let money = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth);
140        let wei_value = money.to_wei();
141
142        assert_eq!(wei_value, U256::from(1_000_000_000_000_000_000_u64));
143    }
144
145    #[rstest]
146    fn test_to_wei_small_amount() {
147        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
148        let money = Money::from_wei(U256::from(1_000_000_000_000_u64), eth);
149        let wei_value = money.to_wei();
150
151        assert_eq!(wei_value, U256::from(1_000_000_000_000_u64));
152    }
153
154    #[rstest]
155    fn test_wei_roundtrip() {
156        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
157        let original_wei = U256::from(1_234_567_890_123_456_789_u64);
158        let money = Money::from_wei(original_wei, eth);
159        let roundtrip_wei = money.to_wei();
160
161        assert_eq!(original_wei, roundtrip_wei);
162    }
163
164    #[rstest]
165    fn test_checked_arith_rejects_mixed_scale_same_code() {
166        // Currency equality compares code only, so an 18-decimal ETH and the standard
167        // 8-decimal ETH compare equal. Their raw values are at different scales, so
168        // checked_add / checked_sub must detect the mismatch and return None.
169        let eth_standard = Currency::ETH(); // precision 8
170        let eth_wei = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
171
172        let standard = Money::new(1.0, eth_standard);
173        let wei = Money::from_wei(U256::from(1_000_000_000_000_000_000_u128), eth_wei);
174
175        assert_eq!(wei.checked_add(standard), None);
176        assert_eq!(standard.checked_add(wei), None);
177        assert_eq!(wei.checked_sub(standard), None);
178        assert_eq!(standard.checked_sub(wei), None);
179    }
180
181    #[rstest]
182    #[should_panic(expected = "`from_wei` requires a currency with precision 18")]
183    fn test_from_wei_rejects_non_18_precision() {
184        let eth_8 = Currency::ETH(); // precision 8
185        let _ = Money::from_wei(U256::from(1_000_000_000_000_000_000_u128), eth_8);
186    }
187
188    #[rstest]
189    #[should_panic(expected = "requires precision 18")]
190    fn test_to_wei_rejects_non_18_precision() {
191        let usd = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat);
192        let m = Money::new(1.0, usd);
193        let _ = m.to_wei();
194    }
195
196    #[rstest]
197    fn test_from_wei_zero() {
198        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
199        let money = Money::from_wei(U256::ZERO, eth);
200
201        assert!(money.is_zero());
202        assert_eq!(money.as_decimal(), Decimal::ZERO);
203        assert_eq!(money.to_wei(), U256::ZERO);
204    }
205
206    // The largest `u128` value does not fit into an *signed* 128-bit integer and therefore must
207    // trigger a safety panic.
208    #[rstest]
209    #[should_panic(expected = "raw wei value exceeds signed 128-bit range")]
210    fn test_from_wei_maximum_u128() {
211        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
212        let max_wei = U256::from(u128::MAX);
213        let _ = Money::from_wei(max_wei, eth);
214    }
215
216    #[rstest]
217    #[should_panic(expected = "raw wei value exceeds 128-bit range")]
218    fn test_from_wei_overflow() {
219        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
220        let overflow_wei = U256::from(u128::MAX) + U256::from(1u64);
221        Money::from_wei(overflow_wei, eth);
222    }
223
224    #[rstest]
225    fn test_from_wei_different_tokens() {
226        let usdc = Currency::new("USDC", 18, 0, "USD Coin", CurrencyType::Crypto);
227        let dai = Currency::new("DAI", 18, 0, "Dai Stablecoin", CurrencyType::Crypto);
228
229        let wei_amount = U256::from(500_000_000_000_000_000_u64); // 0.5 tokens
230        let usdc_money = Money::from_wei(wei_amount, usdc);
231        let dai_money = Money::from_wei(wei_amount, dai);
232
233        assert_eq!(usdc_money.as_decimal(), dai_money.as_decimal());
234        assert_eq!(usdc_money.to_wei(), dai_money.to_wei());
235        assert_ne!(usdc_money.currency, dai_money.currency);
236    }
237
238    #[rstest]
239    fn test_arithmetic_with_wei_values() {
240        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
241        let money1 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
242        let money2 = Money::from_wei(U256::from(500_000_000_000_000_000_u64), eth); // 0.5 ETH
243
244        let sum = money1 + money2;
245        assert_eq!(sum.as_decimal(), dec!(1.5)); // 1.5
246        assert_eq!(sum.to_wei(), U256::from(1_500_000_000_000_000_000_u64));
247
248        let diff = money1 - money2;
249        assert_eq!(diff.as_decimal(), dec!(0.5)); // 0.5
250        assert_eq!(diff.to_wei(), U256::from(500_000_000_000_000_000_u64));
251    }
252
253    #[rstest]
254    fn test_comparison_with_wei_values() {
255        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
256        let money1 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
257        let money2 = Money::from_wei(U256::from(2_000_000_000_000_000_000_u64), eth); // 2 ETH
258        let money3 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
259
260        assert!(money1 < money2);
261        assert!(money2 > money1);
262        assert_eq!(money1, money3);
263        assert!(money1 <= money3);
264        assert!(money1 >= money3);
265    }
266}