1use anychain_core::{Amount, AmountError};
2
3use core::fmt;
4use serde::Serialize;
5use std::ops::{Add, Sub};
6
7const COIN: i64 = 1_0000_0000;
9
10#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
12pub struct BitcoinAmount(pub i64);
13
14pub enum Denomination {
15 Satoshi,
17 MicroBit,
19 MilliBit,
21 CentiBit,
23 DeciBit,
25 Bitcoin,
27}
28
29impl Denomination {
30 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 pub const ZERO: BitcoinAmount = BitcoinAmount(0);
65 pub const ONE_SAT: BitcoinAmount = BitcoinAmount(1);
67 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}