anychain_bitcoin/
amount.rs

1use anychain_core::{Amount, AmountError};
2
3use core::fmt;
4use serde::Serialize;
5use std::ops::{Add, Sub};
6
7// Number of satoshis (base unit) per BTC
8const COIN: i64 = 1_0000_0000;
9
10/// Represents the amount of Bitcoin in satoshis
11#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
12pub struct BitcoinAmount(pub i64);
13
14pub enum Denomination {
15    // sat
16    Satoshi,
17    // uBTC (bit)
18    MicroBit,
19    // mBTC
20    MilliBit,
21    // cBTC
22    CentiBit,
23    // dBTC
24    DeciBit,
25    // BTC
26    Bitcoin,
27}
28
29impl Denomination {
30    /// The number of decimal places more than a satoshi.
31    fn precision(self) -> u32 {
32        match self {
33            Denomination::Satoshi => 0,
34            Denomination::MicroBit => 2,
35            Denomination::MilliBit => 5,
36            Denomination::CentiBit => 6,
37            Denomination::DeciBit => 7,
38            Denomination::Bitcoin => 8,
39        }
40    }
41}
42
43impl fmt::Display for Denomination {
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        write!(
46            f,
47            "{}",
48            match self {
49                Denomination::Satoshi => "satoshi",
50                Denomination::MicroBit => "uBTC",
51                Denomination::MilliBit => "mBTC",
52                Denomination::CentiBit => "cBTC",
53                Denomination::DeciBit => "dBTC",
54                Denomination::Bitcoin => "BTC",
55            }
56        )
57    }
58}
59
60impl Amount for BitcoinAmount {}
61
62impl BitcoinAmount {
63    /// The zero amount.
64    pub const ZERO: BitcoinAmount = BitcoinAmount(0);
65    /// Exactly one satoshi.
66    pub const ONE_SAT: BitcoinAmount = BitcoinAmount(1);
67    /// Exactly one bitcoin.
68    pub const ONE_BTC: BitcoinAmount = BitcoinAmount(COIN);
69
70    pub fn from_satoshi(satoshis: i64) -> Result<Self, AmountError> {
71        Ok(Self(satoshis))
72    }
73
74    pub fn from_ubtc(ubtc_value: i64) -> Result<Self, AmountError> {
75        let satoshis = ubtc_value * 10_i64.pow(Denomination::MicroBit.precision());
76
77        Self::from_satoshi(satoshis)
78    }
79
80    pub fn from_mbtc(mbtc_value: i64) -> Result<Self, AmountError> {
81        let satoshis = mbtc_value * 10_i64.pow(Denomination::MilliBit.precision());
82
83        Self::from_satoshi(satoshis)
84    }
85
86    pub fn from_cbtc(cbtc_value: i64) -> Result<Self, AmountError> {
87        let satoshis = cbtc_value * 10_i64.pow(Denomination::CentiBit.precision());
88
89        Self::from_satoshi(satoshis)
90    }
91
92    pub fn from_dbtc(dbtc_value: i64) -> Result<Self, AmountError> {
93        let satoshis = dbtc_value * 10_i64.pow(Denomination::DeciBit.precision());
94
95        Self::from_satoshi(satoshis)
96    }
97
98    pub fn from_btc(btc_value: i64) -> Result<Self, AmountError> {
99        let satoshis = btc_value * 10_i64.pow(Denomination::Bitcoin.precision());
100
101        Self::from_satoshi(satoshis)
102    }
103}
104
105impl Add for BitcoinAmount {
106    type Output = Result<Self, AmountError>;
107    fn add(self, rhs: Self) -> Self::Output {
108        Self::from_satoshi(self.0 + rhs.0)
109    }
110}
111
112impl Sub for BitcoinAmount {
113    type Output = Result<Self, AmountError>;
114    fn sub(self, rhs: Self) -> Self::Output {
115        Self::from_satoshi(self.0 - rhs.0)
116    }
117}
118
119impl fmt::Display for BitcoinAmount {
120    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121        write!(f, "{}", self.0)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn test_from_satoshi(sat_value: i64, expected_amount: BitcoinAmount) {
130        let amount = BitcoinAmount::from_satoshi(sat_value).unwrap();
131        assert_eq!(expected_amount, amount)
132    }
133
134    fn test_from_ubtc(ubtc_value: i64, expected_amount: BitcoinAmount) {
135        let amount = BitcoinAmount::from_ubtc(ubtc_value).unwrap();
136        assert_eq!(expected_amount, amount)
137    }
138
139    fn test_from_mbtc(mbtc_value: i64, expected_amount: BitcoinAmount) {
140        let amount = BitcoinAmount::from_mbtc(mbtc_value).unwrap();
141        assert_eq!(expected_amount, amount)
142    }
143
144    fn test_from_cbtc(cbtc_value: i64, expected_amount: BitcoinAmount) {
145        let amount = BitcoinAmount::from_cbtc(cbtc_value).unwrap();
146        assert_eq!(expected_amount, amount)
147    }
148
149    fn test_from_dbtc(dbtc_value: i64, expected_amount: BitcoinAmount) {
150        let amount = BitcoinAmount::from_dbtc(dbtc_value).unwrap();
151        assert_eq!(expected_amount, amount)
152    }
153
154    fn test_from_btc(btc_value: i64, expected_amount: BitcoinAmount) {
155        let amount = BitcoinAmount::from_btc(btc_value).unwrap();
156        assert_eq!(expected_amount, amount)
157    }
158
159    fn test_addition(a: &i64, b: &i64, result: &i64) {
160        let a = BitcoinAmount::from_satoshi(*a).unwrap();
161        let b = BitcoinAmount::from_satoshi(*b).unwrap();
162        let result = BitcoinAmount::from_satoshi(*result).unwrap();
163
164        assert_eq!(result, a.add(b).unwrap());
165    }
166
167    fn test_subtraction(a: &i64, b: &i64, result: &i64) {
168        let a = BitcoinAmount::from_satoshi(*a).unwrap();
169        let b = BitcoinAmount::from_satoshi(*b).unwrap();
170        let result = BitcoinAmount::from_satoshi(*result).unwrap();
171
172        assert_eq!(result, a.sub(b).unwrap());
173    }
174
175    pub struct AmountDenominationTestCase {
176        satoshi: i64,
177        micro_bit: i64,
178        milli_bit: i64,
179        centi_bit: i64,
180        deci_bit: i64,
181        bitcoin: i64,
182    }
183
184    mod valid_conversions {
185        use super::*;
186
187        const TEST_AMOUNTS: [AmountDenominationTestCase; 5] = [
188            AmountDenominationTestCase {
189                satoshi: 0,
190                micro_bit: 0,
191                milli_bit: 0,
192                centi_bit: 0,
193                deci_bit: 0,
194                bitcoin: 0,
195            },
196            AmountDenominationTestCase {
197                satoshi: 100000000,
198                micro_bit: 1000000,
199                milli_bit: 1000,
200                centi_bit: 100,
201                deci_bit: 10,
202                bitcoin: 1,
203            },
204            AmountDenominationTestCase {
205                satoshi: 100000000000,
206                micro_bit: 1000000000,
207                milli_bit: 1000000,
208                centi_bit: 100000,
209                deci_bit: 10000,
210                bitcoin: 1000,
211            },
212            AmountDenominationTestCase {
213                satoshi: 123456700000000,
214                micro_bit: 1234567000000,
215                milli_bit: 1234567000,
216                centi_bit: 123456700,
217                deci_bit: 12345670,
218                bitcoin: 1234567,
219            },
220            AmountDenominationTestCase {
221                satoshi: 2100000000000000,
222                micro_bit: 21000000000000,
223                milli_bit: 21000000000,
224                centi_bit: 2100000000,
225                deci_bit: 210000000,
226                bitcoin: 21000000,
227            },
228        ];
229
230        #[test]
231        fn test_satoshi_conversion() {
232            TEST_AMOUNTS.iter().for_each(|amounts| {
233                test_from_satoshi(amounts.satoshi, BitcoinAmount(amounts.satoshi))
234            });
235        }
236
237        #[test]
238        fn test_ubtc_conversion() {
239            TEST_AMOUNTS.iter().for_each(|amounts| {
240                test_from_ubtc(amounts.micro_bit, BitcoinAmount(amounts.satoshi))
241            });
242        }
243
244        #[test]
245        fn test_mbtc_conversion() {
246            TEST_AMOUNTS.iter().for_each(|amounts| {
247                test_from_mbtc(amounts.milli_bit, BitcoinAmount(amounts.satoshi))
248            });
249        }
250
251        #[test]
252        fn test_cbtc_conversion() {
253            TEST_AMOUNTS.iter().for_each(|amounts| {
254                test_from_cbtc(amounts.centi_bit, BitcoinAmount(amounts.satoshi))
255            });
256        }
257
258        #[test]
259        fn test_dbtc_conversion() {
260            TEST_AMOUNTS.iter().for_each(|amounts| {
261                test_from_dbtc(amounts.deci_bit, BitcoinAmount(amounts.satoshi))
262            });
263        }
264
265        #[test]
266        fn test_btc_conversion() {
267            TEST_AMOUNTS
268                .iter()
269                .for_each(|amounts| test_from_btc(amounts.bitcoin, BitcoinAmount(amounts.satoshi)));
270        }
271    }
272
273    mod valid_arithmetic {
274        use super::*;
275
276        const TEST_VALUES: [(i64, i64, i64); 7] = [
277            (0, 0, 0),
278            (1, 2, 3),
279            (100000, 0, 100000),
280            (123456789, 987654321, 1111111110),
281            (100000000000000, 2000000000000000, 2100000000000000),
282            (-100000000000000, -2000000000000000, -2100000000000000),
283            (1000000, -1000000, 0),
284        ];
285
286        #[test]
287        fn test_valid_addition() {
288            TEST_VALUES
289                .iter()
290                .for_each(|(a, b, c)| test_addition(a, b, c));
291        }
292
293        #[test]
294        fn test_valid_subtraction() {
295            TEST_VALUES
296                .iter()
297                .for_each(|(a, b, c)| test_subtraction(c, b, a));
298        }
299    }
300
301    mod test_invalid {
302        use super::*;
303
304        mod test_invalid_conversion {
305            use super::*;
306
307            const INVALID_TEST_AMOUNTS: [AmountDenominationTestCase; 4] = [
308                AmountDenominationTestCase {
309                    satoshi: 1,
310                    micro_bit: 1,
311                    milli_bit: 1,
312                    centi_bit: 1,
313                    deci_bit: 1,
314                    bitcoin: 1,
315                },
316                AmountDenominationTestCase {
317                    satoshi: 1,
318                    micro_bit: 10,
319                    milli_bit: 100,
320                    centi_bit: 1000,
321                    deci_bit: 1000000,
322                    bitcoin: 100000000,
323                },
324                AmountDenominationTestCase {
325                    satoshi: 123456789,
326                    micro_bit: 1234567,
327                    milli_bit: 1234,
328                    centi_bit: 123,
329                    deci_bit: 12,
330                    bitcoin: 1,
331                },
332                AmountDenominationTestCase {
333                    satoshi: 2100000000000000,
334                    micro_bit: 21000000000000,
335                    milli_bit: 21000000000,
336                    centi_bit: 2100000000,
337                    deci_bit: 210000000,
338                    bitcoin: 20999999,
339                },
340            ];
341
342            #[should_panic]
343            #[test]
344            fn test_invalid_ubtc_conversion() {
345                INVALID_TEST_AMOUNTS.iter().for_each(|amounts| {
346                    test_from_ubtc(amounts.micro_bit, BitcoinAmount(amounts.satoshi))
347                });
348            }
349
350            #[should_panic]
351            #[test]
352            fn test_invalid_mbtc_conversion() {
353                INVALID_TEST_AMOUNTS.iter().for_each(|amounts| {
354                    test_from_mbtc(amounts.milli_bit, BitcoinAmount(amounts.satoshi))
355                });
356            }
357
358            #[should_panic]
359            #[test]
360            fn test_invalid_cbtc_conversion() {
361                INVALID_TEST_AMOUNTS.iter().for_each(|amounts| {
362                    test_from_cbtc(amounts.centi_bit, BitcoinAmount(amounts.satoshi))
363                });
364            }
365
366            #[should_panic]
367            #[test]
368            fn test_invalid_dbtc_conversion() {
369                INVALID_TEST_AMOUNTS.iter().for_each(|amounts| {
370                    test_from_dbtc(amounts.deci_bit, BitcoinAmount(amounts.satoshi))
371                });
372            }
373
374            #[should_panic]
375            #[test]
376            fn test_invalid_btc_conversion() {
377                INVALID_TEST_AMOUNTS.iter().for_each(|amounts| {
378                    test_from_btc(amounts.bitcoin, BitcoinAmount(amounts.satoshi))
379                });
380            }
381        }
382
383        mod invalid_arithmetic {
384            use super::*;
385
386            const TEST_VALUES: [(i64, i64, i64); 8] = [
387                (0, 0, 1),
388                (1, 2, 5),
389                (100000, 1, 100000),
390                (123456789, 123456789, 123456789),
391                (-1000, -1000, 2000),
392                (2100000000000000, 1, 2100000000000001),
393                (2100000000000000, 2100000000000000, 4200000000000000),
394                (-2100000000000000, -2100000000000000, -4200000000000000),
395            ];
396
397            #[should_panic]
398            #[test]
399            fn test_invalid_addition() {
400                TEST_VALUES
401                    .iter()
402                    .for_each(|(a, b, c)| test_addition(a, b, c));
403            }
404
405            #[should_panic]
406            #[test]
407            fn test_invalid_subtraction() {
408                TEST_VALUES
409                    .iter()
410                    .for_each(|(a, b, c)| test_subtraction(a, b, c));
411            }
412        }
413    }
414}