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;
19
20use crate::types::{Currency, Money};
21
22impl Money {
23    /// Creates a new [`Money`] instance from raw wei value with 18-decimal precision.
24    ///
25    /// This method is specifically designed for DeFi applications where values are
26    /// represented in wei (the smallest unit of Ether, 1 ETH = 10^18 wei).
27    ///
28    /// # Panics
29    ///
30    /// Panics if the raw wei value exceeds 128-bit range.
31    pub fn from_wei<U>(raw_wei: U, currency: Currency) -> Self
32    where
33        U: Into<U256>,
34    {
35        let raw_u256: U256 = raw_wei.into();
36        let raw_u128: u128 = raw_u256
37            .try_into()
38            .expect("raw wei value exceeds 128-bit range");
39
40        assert!(
41            raw_u128 <= i128::MAX as u128,
42            "raw wei value exceeds signed 128-bit range"
43        );
44
45        let raw_i128: i128 = raw_u128 as i128;
46        Self::from_raw(raw_i128, currency)
47    }
48
49    /// Converts this [`Money`] instance to raw wei value.
50    ///
51    /// Only valid for prices with precision 18. For other precisions convert to precision 18 first.
52    ///
53    /// # Returns
54    ///
55    /// The raw wei value as a U256.
56    pub fn to_wei(&self) -> U256 {
57        U256::from(self.raw as u128)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use alloy_primitives::U256;
64    use rstest::rstest;
65    use rust_decimal::Decimal;
66    use rust_decimal_macros::dec;
67
68    use super::*;
69    use crate::enums::CurrencyType;
70
71    #[rstest]
72    fn test_from_wei_one_eth() {
73        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
74        let one_eth_wei = U256::from(1_000_000_000_000_000_000_u64);
75        let money = Money::from_wei(one_eth_wei, eth);
76
77        // Use decimal comparison for high precision values
78        assert_eq!(money.as_decimal(), dec!(1));
79        assert_eq!(money.currency.precision, 18);
80    }
81
82    #[rstest]
83    fn test_from_wei_small_amount() {
84        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
85        let small_wei = U256::from(1_000_000_000_000_u64); // 0.000001 ETH
86        let money = Money::from_wei(small_wei, eth);
87
88        // Use decimal comparison for high precision values
89        assert_eq!(money.as_decimal(), dec!(0.000001)); // 0.000001
90    }
91
92    #[rstest]
93    fn test_to_wei_one_eth() {
94        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
95        let money = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth);
96        let wei_value = money.to_wei();
97
98        assert_eq!(wei_value, U256::from(1_000_000_000_000_000_000_u64));
99    }
100
101    #[rstest]
102    fn test_to_wei_small_amount() {
103        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
104        let money = Money::from_wei(U256::from(1_000_000_000_000_u64), eth);
105        let wei_value = money.to_wei();
106
107        assert_eq!(wei_value, U256::from(1_000_000_000_000_u64));
108    }
109
110    #[rstest]
111    fn test_wei_roundtrip() {
112        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
113        let original_wei = U256::from(1_234_567_890_123_456_789_u64);
114        let money = Money::from_wei(original_wei, eth);
115        let roundtrip_wei = money.to_wei();
116
117        assert_eq!(original_wei, roundtrip_wei);
118    }
119
120    #[rstest]
121    fn test_from_wei_zero() {
122        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
123        let money = Money::from_wei(U256::ZERO, eth);
124
125        assert!(money.is_zero());
126        assert_eq!(money.as_decimal(), Decimal::ZERO);
127        assert_eq!(money.to_wei(), U256::ZERO);
128    }
129
130    // The largest `u128` value does not fit into an *signed* 128-bit integer and therefore must
131    // trigger a safety panic.
132    #[rstest]
133    #[should_panic(expected = "raw wei value exceeds signed 128-bit range")]
134    fn test_from_wei_maximum_u128() {
135        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
136        let max_wei = U256::from(u128::MAX);
137        let _ = Money::from_wei(max_wei, eth);
138    }
139
140    #[rstest]
141    #[should_panic(expected = "raw wei value exceeds 128-bit range")]
142    fn test_from_wei_overflow() {
143        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
144        let overflow_wei = U256::from(u128::MAX) + U256::from(1u64);
145        Money::from_wei(overflow_wei, eth);
146    }
147
148    #[rstest]
149    fn test_from_wei_different_tokens() {
150        let usdc = Currency::new("USDC", 18, 0, "USD Coin", CurrencyType::Crypto);
151        let dai = Currency::new("DAI", 18, 0, "Dai Stablecoin", CurrencyType::Crypto);
152
153        let wei_amount = U256::from(500_000_000_000_000_000_u64); // 0.5 tokens
154        let usdc_money = Money::from_wei(wei_amount, usdc);
155        let dai_money = Money::from_wei(wei_amount, dai);
156
157        assert_eq!(usdc_money.as_decimal(), dai_money.as_decimal());
158        assert_eq!(usdc_money.to_wei(), dai_money.to_wei());
159        assert_ne!(usdc_money.currency, dai_money.currency);
160    }
161
162    #[rstest]
163    fn test_arithmetic_with_wei_values() {
164        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
165        let money1 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
166        let money2 = Money::from_wei(U256::from(500_000_000_000_000_000_u64), eth); // 0.5 ETH
167
168        let sum = money1 + money2;
169        assert_eq!(sum.as_decimal(), dec!(1.5)); // 1.5
170        assert_eq!(sum.to_wei(), U256::from(1_500_000_000_000_000_000_u64));
171
172        let diff = money1 - money2;
173        assert_eq!(diff.as_decimal(), dec!(0.5)); // 0.5
174        assert_eq!(diff.to_wei(), U256::from(500_000_000_000_000_000_u64));
175    }
176
177    #[rstest]
178    fn test_comparison_with_wei_values() {
179        let eth = Currency::new("ETH", 18, 0, "Ethereum", CurrencyType::Crypto);
180        let money1 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
181        let money2 = Money::from_wei(U256::from(2_000_000_000_000_000_000_u64), eth); // 2 ETH
182        let money3 = Money::from_wei(U256::from(1_000_000_000_000_000_000_u64), eth); // 1 ETH
183
184        assert!(money1 < money2);
185        assert!(money2 > money1);
186        assert_eq!(money1, money3);
187        assert!(money1 <= money3);
188        assert!(money1 >= money3);
189    }
190}