dev
dart 496 lines 18.6 KB
Raw
1 import "package:cw_core/amount/money.dart";
2 import "package:cw_core/crypto_currency.dart";
3 import "package:flutter_test/flutter_test.dart";
4
5 import "utils.dart";
6
7 void main() {
8 group("Money", () {
9 test("parse", () {
10 var money = Money.parse("1", CryptoCurrency.btc);
11 expect(money.amount, BigInt.from(100000000));
12
13 money = Money.parse("-1", CryptoCurrency.btc);
14 expect(money.amount, BigInt.from(-100000000));
15
16 money = Money.parse("1.11", CryptoCurrency.btc);
17 expect(money.amount, BigInt.from(111000000));
18
19 money = Money.parse("-1.11", CryptoCurrency.btc);
20 expect(money.amount, BigInt.from(-111000000));
21
22 money = Money.parse("-.11", CryptoCurrency.btc);
23 expect(money.amount, BigInt.from(-11000000));
24
25 money = Money.parse(".11", CryptoCurrency.btc);
26 expect(money.amount, BigInt.from(11000000));
27
28 // Invalid representation
29 expect(() => Money.parse("1,11", CryptoCurrency.btc), throwsFormatException);
30
31 // To many decimals
32 expect(() => Money.parse("-1.0000000000000001", CryptoCurrency.btc), throwsFormatException);
33
34 money = Money.parse("1", CryptoCurrency.btc, isBaseUnit: true);
35 expect(money.amount, BigInt.from(1));
36
37 money = Money.parse("0", CryptoCurrency.btc, isBaseUnit: true);
38 expect(money.amount, BigInt.from(0));
39
40 money = Money.parse("-1", CryptoCurrency.btc, isBaseUnit: true);
41 expect(money.amount, BigInt.from(-1));
42
43 // canonical representation when expecting base units
44 expect(() => Money.parse("1.0", CryptoCurrency.btc, isBaseUnit: true), throwsFormatException);
45 });
46
47 test("tryParse", () {
48 var money = Money.tryParse("1", CryptoCurrency.btc);
49 expect(money?.amount, BigInt.from(100000000));
50
51 money = Money.tryParse("-1", CryptoCurrency.btc);
52 expect(money?.amount, BigInt.from(-100000000));
53
54 money = Money.tryParse("1.11", CryptoCurrency.btc);
55 expect(money?.amount, BigInt.from(111000000));
56
57 money = Money.tryParse("-1.11", CryptoCurrency.btc);
58 expect(money?.amount, BigInt.from(-111000000));
59
60 money = Money.tryParse("-.11", CryptoCurrency.btc);
61 expect(money?.amount, BigInt.from(-11000000));
62
63 money = Money.tryParse(".11", CryptoCurrency.btc);
64 expect(money?.amount, BigInt.from(11000000));
65
66 // Invalid representation
67 money = Money.tryParse("1,11", CryptoCurrency.btc);
68 expect(money?.amount, isNull);
69
70 // To many decimals
71 money = Money.tryParse("-1.0000000000000001", CryptoCurrency.btc);
72 expect(money?.amount, isNull);
73
74 money = Money.tryParse("1", CryptoCurrency.btc, isBaseUnit: true);
75 expect(money?.amount, BigInt.from(1));
76
77 money = Money.tryParse("0", CryptoCurrency.btc, isBaseUnit: true);
78 expect(money?.amount, BigInt.from(0));
79
80 money = Money.tryParse("-1", CryptoCurrency.btc, isBaseUnit: true);
81 expect(money?.amount, BigInt.from(-1));
82
83 // canonical representation when expecting base units
84 money = Money.tryParse("1.0", CryptoCurrency.btc, isBaseUnit: true);
85 expect(money?.amount, isNull);
86 });
87
88 test("sign", () {
89 expect(Money(BigInt.from(0), CryptoCurrency.btc).sign, 0);
90
91 expect(Money(BigInt.from(1), CryptoCurrency.btc).sign, 1);
92
93 expect(Money(BigInt.from(10), CryptoCurrency.btc).sign, 1);
94
95 expect(Money(BigInt.from(-1), CryptoCurrency.btc).sign, -1);
96
97 expect(Money(BigInt.from(-10), CryptoCurrency.btc).sign, -1);
98 });
99
100 test("isZero", () {
101 expect(Money(BigInt.from(0), CryptoCurrency.btc).isZero, isTrue);
102
103 expect(Money(BigInt.from(1), CryptoCurrency.btc).isZero, isFalse);
104
105 expect(Money(BigInt.from(-1), CryptoCurrency.btc).isZero, isFalse);
106 });
107
108 test("isNegative", () {
109 expect(Money(BigInt.from(0), CryptoCurrency.btc).isNegative, isFalse);
110
111 expect(Money(BigInt.from(1), CryptoCurrency.btc).isNegative, isFalse);
112
113 expect(Money(BigInt.from(-1), CryptoCurrency.btc).isNegative, isTrue);
114 });
115
116 group("Comparison", () {
117 final fourBitcoin = Money(BigInt.from(400000000), CryptoCurrency.btc);
118 final fiveBitcoin = Money(BigInt.from(500000000), CryptoCurrency.btc);
119 final sixBitcoin = Money(BigInt.from(600000000), CryptoCurrency.btc);
120
121 final fiveMonero = Money.parse("5", CryptoCurrency.xmr);
122
123 test("==", () {
124 expect(fiveBitcoin, equals(Money(BigInt.from(500000000), CryptoCurrency.btc)));
125 expect(fiveBitcoin, isNot(equals(fourBitcoin)));
126 expect(fiveBitcoin, isNot(equals(sixBitcoin)));
127 expect(fiveBitcoin, isNot(equals(fiveMonero)));
128 // intentional check of incompatible types.
129 // ignore: unrelated_type_equality_checks
130 expect(fiveBitcoin == "Not money", equals(false));
131 });
132
133 test("<", () {
134 expect(fiveBitcoin < sixBitcoin, isTrue);
135 expect(fiveBitcoin < fiveBitcoin, isFalse);
136 expect(fiveBitcoin < fourBitcoin, isFalse);
137
138 // Cannot compare money in different currencies:
139 expect(() => fiveBitcoin < fiveMonero, throwsArgumentError);
140 });
141
142 test("<=", () {
143 expect(fiveBitcoin <= sixBitcoin, isTrue);
144 expect(fiveBitcoin <= fiveBitcoin, isTrue);
145 expect(fiveBitcoin <= fourBitcoin, isFalse);
146
147 // Cannot compare money in different currencies:
148 expect(() => fiveBitcoin <= fiveMonero, throwsArgumentError);
149 });
150
151 test(">", () {
152 expect(fiveBitcoin > fourBitcoin, isTrue);
153 expect(fiveBitcoin > fiveBitcoin, isFalse);
154 expect(fiveBitcoin > sixBitcoin, isFalse);
155
156 // Cannot compare money in different currencies:
157 expect(() => fiveBitcoin > fiveMonero, throwsArgumentError);
158 });
159
160 test(">=", () {
161 expect(fiveBitcoin >= fourBitcoin, isTrue);
162 expect(fiveBitcoin >= fiveBitcoin, isTrue);
163 expect(fiveBitcoin >= sixBitcoin, isFalse);
164
165 // Cannot compare money in different currencies:
166 expect(() => fiveBitcoin >= fiveMonero, throwsArgumentError);
167 });
168
169 test("conformance to Comparable", () {
170 expect(fiveBitcoin, isA<Comparable<Money>>());
171
172 expect(fiveBitcoin.compareTo(fiveBitcoin), isZero);
173 expect(fiveBitcoin.compareTo(fourBitcoin), isPositive);
174 expect(fiveBitcoin.compareTo(sixBitcoin), isNegative);
175 expect(() => fiveBitcoin.compareTo(fiveMonero), throwsArgumentError);
176 });
177 });
178
179 group("Math", () {
180 test("+", () {
181 final oneBitcoin = Money.parse("1", CryptoCurrency.btc);
182 final twoBitcoin = Money.parse("2", CryptoCurrency.btc);
183 final threeBitcoin = Money.parse("3", CryptoCurrency.btc);
184
185 expect(oneBitcoin + twoBitcoin, equals(threeBitcoin));
186 });
187
188 test("cannot add different currencies", () {
189 final oneBitcoin = Money(BigInt.one, CryptoCurrency.btc);
190 final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
191
192 expect(() => oneBitcoin + oneMonero, throwsArgumentError);
193 });
194
195 test("invert money", () {
196 final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
197 final negativeOneSatoshi = Money(BigInt.from(-1), CryptoCurrency.btc);
198
199 expect(-oneSatoshi, equals(negativeOneSatoshi));
200 expect(-negativeOneSatoshi, equals(oneSatoshi));
201 });
202
203 test("-", () {
204 final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
205 final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
206 final threeSatoshi = Money(BigInt.from(3), CryptoCurrency.btc);
207
208 expect(threeSatoshi - oneSatoshi, equals(twoSatoshi));
209 });
210
211 test("cannot subtract different currencies", () {
212 final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
213 final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
214
215 expect(() => oneSatoshi - oneMonero, throwsArgumentError);
216 });
217
218 test("*", () {
219 final zeroSatoshi = Money(BigInt.zero, CryptoCurrency.btc);
220 final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
221 final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
222
223 expect(oneSatoshi * BigInt.zero, equals(zeroSatoshi));
224 expect(oneSatoshi * BigInt.two, equals(twoSatoshi));
225 expect(oneSatoshi * BigInt.from(-2), equals(-twoSatoshi));
226 });
227
228 group("/", () {
229 final threeBI = BigInt.from(3);
230 final fourBI = BigInt.from(4);
231
232 final negThreeBI = BigInt.from(-3);
233 final negFourBI = BigInt.from(-4);
234
235 test("rounds down when fractional part is < 0.5", () {
236 expect((Money(BigInt.from(10), CryptoCurrency.btc) / threeBI).amount, threeBI);
237 expect((Money(BigInt.from(-10), CryptoCurrency.btc) / threeBI).amount, negThreeBI);
238 expect((Money(BigInt.from(10), CryptoCurrency.btc) / negThreeBI).amount, negThreeBI);
239 expect((Money(BigInt.from(-10), CryptoCurrency.btc) / negThreeBI).amount, threeBI);
240 });
241
242 test("rounds up when fractional part is > 0.5", () {
243 expect((Money(BigInt.from(11), CryptoCurrency.btc) / threeBI).amount, fourBI); // 3.66...
244 expect((Money(BigInt.from(-11), CryptoCurrency.btc) / threeBI).amount, negFourBI);
245 expect((Money(BigInt.from(11), CryptoCurrency.btc) / negThreeBI).amount, negFourBI);
246 expect((Money(BigInt.from(-11), CryptoCurrency.btc) / negThreeBI).amount, fourBI);
247 });
248
249 test("rounds AWAY from zero when fractional part is exactly 0.5 (Ties)", () {
250 // 5 / 2 = 2.5 -> 3
251 expect((Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.two).amount, threeBI);
252 // -5 / 2 = -2.5 -> -3
253 expect((Money(BigInt.from(-5), CryptoCurrency.btc) / BigInt.two).amount, negThreeBI);
254 // 5 / -2 = -2.5 -> -3
255 expect((Money(BigInt.from(5), CryptoCurrency.btc) / -BigInt.two).amount, negThreeBI);
256 // -5 / -2 = 2.5 -> 3
257 expect((Money(BigInt.from(-5), CryptoCurrency.btc) / -BigInt.two).amount, threeBI);
258 });
259
260 test("handles zero numerator correctly", () {
261 expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(5)).amount, BigInt.zero);
262 expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(-5)).amount, BigInt.zero);
263 });
264
265 test("throws Exception on division by zero", () {
266 expect(
267 () => Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.zero,
268 throwsA(isA<Exception>()),
269 );
270 });
271
272 test("handles very large BigInts without precision loss", () {
273 // A number that would lose precision if converted to a standard double
274 final hugeA = BigInt.parse("10000000000000000000000000000000000005");
275 final hugeB = BigInt.parse("10");
276 // 1000...005 / 10 = 1000...000.5 -> Should round up to 1000...001
277 final expected = BigInt.parse("1000000000000000000000000000000000001");
278
279 expect((Money(hugeA, CryptoCurrency.btc) / hugeB).amount, expected);
280 });
281
282 test("handles exact division perfectly", () {
283 expect(
284 (Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
285 BigInt.from(5),
286 );
287 expect(
288 (Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
289 BigInt.from(-5),
290 );
291 });
292 });
293 });
294
295 group("copyWith", () {
296 final money = Money.parse("1", CryptoCurrency.btc);
297
298 test("currency", () {
299 final copy = money.copyWith(currency: CryptoCurrency.eth);
300 expect(copy.amount, equals(BigInt.parse("1000000000000000000")));
301 expect(copy.currency.decimals, equals(18));
302 expect(copy.toString(), "1");
303 });
304
305 test("currency nano", () {
306 final copy = money.copyWith(currency: CryptoCurrency.nano);
307 expect(copy.amount, equals(BigInt.parse("1000000000000000000000000000000")));
308 expect(copy.currency.decimals, equals(30));
309 expect(copy.toString(), "1");
310 });
311
312 test("amount", () {
313 final copy = money.copyWith(amount: BigInt.one);
314 expect(copy.amount, BigInt.one);
315 expect(copy.currency.decimals, 8);
316 });
317
318 test("amount and currency", () {
319 final copy = money.copyWith(amount: BigInt.one);
320 expect(copy.amount, BigInt.one);
321 expect(copy.currency.decimals, 8);
322 });
323 });
324
325 group("toStringWithPrecision", () {
326 test("with fractionalDigits and padded with zeros", () {
327 final money = Money.parse("1", CryptoCurrency.btc);
328 expect(money.amount, equals(BigInt.parse("100000000")));
329 expect(money.currency.decimals, equals(8));
330 expect(money.toStringWithPrecision(fractionalDigits: 5, trimZeros: false), "1.00000");
331 });
332
333 test("using base unit sats", () {
334 final money = Money.parse("1", CryptoCurrency.btc);
335 expect(money.amount, equals(BigInt.parse("100000000")));
336 expect(money.currency.decimals, equals(8));
337 expect(money.toStringWithPrecision(useBaseUnit: true), "100000000");
338 });
339 });
340
341 group("toStringWithSymbol", () {
342 test("with fractionalDigits and padded with zeros", () {
343 final money = Money.parse("1", CryptoCurrency.btc);
344 expect(money.amount, equals(BigInt.parse("100000000")));
345 expect(money.currency.decimals, equals(8));
346 expect(money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false), "1.00000 BTC");
347 });
348
349 test("using base unit sats", () {
350 final money = Money.parse("1", CryptoCurrency.btc);
351 expect(money.amount, equals(BigInt.parse("100000000")));
352 expect(money.currency.decimals, equals(8));
353 expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats");
354 });
355
356 group("withSymbolPrefix", () {
357 test("with fractionalDigits and padded with zeros", () {
358 final money = Money.parse("1", CryptoCurrency.btc);
359 expect(money.amount, equals(BigInt.parse("100000000")));
360 expect(money.currency.decimals, equals(8));
361 expect(
362 money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false, withSymbolPrefix: true),
363 "BTC 1.00000",
364 );
365 });
366
367 test("using base unit sats", () {
368 final money = Money.parse("1", CryptoCurrency.btc);
369 expect(money.amount, equals(BigInt.parse("100000000")));
370 expect(money.currency.decimals, equals(8));
371 expect(
372 money.toStringWithSymbol(useBaseUnit: true, withSymbolPrefix: true),
373 "sats 100000000",
374 );
375 });
376 });
377 });
378
379 group("different scales", () {
380 final coarseOne = Money(BigInt.one, CryptoCurrency.btc, 0);
381 final fineOne = Money(BigInt.from(100000000), CryptoCurrency.btc, 8);
382
383 test("== across scales", () {
384 expect(coarseOne, equals(fineOne));
385 expect(coarseOne, isNot(equals(Money(BigInt.two, CryptoCurrency.btc, 0))));
386 });
387
388 test("equal values across scales hash equally", () {
389 expect(coarseOne.hashCode, equals(fineOne.hashCode));
390
391 expect(
392 Money(BigInt.from(-11), CryptoCurrency.btc, 1).hashCode,
393 equals(Money(BigInt.from(-110000000), CryptoCurrency.btc, 8).hashCode),
394 );
395 });
396
397 test("zero hashes equally at every scale", () {
398 final hashes = [0, 1, 8, 18, 30]
399 .map((s) => Money(BigInt.zero, CryptoCurrency.btc, s).hashCode)
400 .toSet();
401
402 expect(hashes, hasLength(1));
403 });
404
405 test("whole units are not confused with their digits", () {
406 // (100, scale 0) is one hundred, not one: stripping must stop at 0.
407 final oneHundred = Money(BigInt.from(100), CryptoCurrency.btc, 0);
408
409 expect(oneHundred, isNot(equals(coarseOne)));
410 expect(oneHundred.hashCode, isNot(equals(coarseOne.hashCode)));
411 });
412
413 test("Set deduplicates across scales", () {
414 expect(
415 {
416 Money(BigInt.from(11), CryptoCurrency.btc, 1),
417 Money(BigInt.from(110000000), CryptoCurrency.btc, 8),
418 Money(BigInt.from(1100), CryptoCurrency.btc, 3),
419 },
420 hasLength(1),
421 );
422 });
423
424 test("+ aligns operands", () {
425 expect(coarseOne + fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0)));
426 expect(coarseOne + fineOne, equals(fineOne + coarseOne));
427 });
428
429 test("- aligns operands", () {
430 // Regression: this used to subtract the raw amounts, i.e. 1 - 100000000.
431 expect((coarseOne - fineOne).isZero, isTrue);
432 expect((fineOne - coarseOne).isZero, isTrue);
433
434 final threeCoarse = Money(BigInt.from(3), CryptoCurrency.btc, 0);
435 expect(threeCoarse - fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0)));
436 expect(threeCoarse - fineOne, equals(-(fineOne - threeCoarse)));
437 });
438
439 test("the result keeps the finer scale", () {
440 expect((coarseOne + fineOne).decimals, greaterThanOrEqualTo(fineOne.decimals));
441 });
442
443 test("sub-unit precision survives a coarse operand", () {
444 final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc, 8);
445
446 expect(
447 coarseOne + oneSatoshi,
448 equals(Money(BigInt.from(100000001), CryptoCurrency.btc, 8)),
449 );
450 });
451
452 test("comparison operators align", () {
453 expect(coarseOne < fineOne, isFalse);
454 expect(coarseOne <= fineOne, isTrue);
455 expect(coarseOne >= fineOne, isTrue);
456 expect(coarseOne > fineOne, isFalse);
457 expect(Money(BigInt.two, CryptoCurrency.btc, 0) > fineOne, isTrue);
458 });
459
460 test("compareTo aligns and agrees with ==", () {
461 expect(coarseOne.compareTo(fineOne), isZero);
462 expect(coarseOne.compareTo(fineOne) == 0, equals(coarseOne == fineOne));
463 expect(Money(BigInt.two, CryptoCurrency.btc, 0).compareTo(fineOne), isPositive);
464 expect(Money(BigInt.zero, CryptoCurrency.btc, 0).compareTo(fineOne), isNegative);
465 });
466
467 test("toStringWithPrecision", () {
468 expect(
469 Money.parse("60000.123456", USD, strictParsing: false).toStringWithPrecision(),
470 "60000.12",
471 );
472 expect(
473 Money.parse("1.23456789", USD, strictParsing: false).toStringWithPrecision(),
474 "1.23",
475 );
476 expect(Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(), "1");
477 expect(
478 Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(useBaseUnit: true),
479 "100000000",
480 );
481 });
482
483 test("toString", () {
484 expect(
485 Money.parse("60000.123456", CryptoCurrency.btc, strictParsing: false).toString(),
486 "60000.123456",
487 );
488 expect(
489 Money.parse("1.23456789", CryptoCurrency.btc, strictParsing: false).toString(),
490 "1.23456789",
491 );
492 expect(Money(BigInt.one, CryptoCurrency.btc, 0).toString(), "1");
493 });
494 });
495 });
496 }