-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CoinUnit class to convert satoshis and coin units
- Loading branch information
Showing
3 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/// Thrown when a number does not match the expected format for a given | ||
/// [CoinUnit]. | ||
class BadAmountString implements Exception {} | ||
|
||
/// Objects of this class represent a coin denomination with a given number of | ||
/// [decimals]. Use [coin] for whole coins with 6 decimal places and [sats] for | ||
/// the smallest unit with no decimal places. | ||
class CoinUnit { | ||
|
||
static final _numberRegex = RegExp(r"^\d+(\.\d+)?$"); | ||
static final _trailZeroRegex = RegExp(r"\.?0*$"); | ||
|
||
/// The number of decimal places for this unit | ||
final int decimals; | ||
/// The number of satoshis per unit | ||
final BigInt satsPerUnit; | ||
|
||
/// Creates a unit with a given number of [decimals]. | ||
CoinUnit(this.decimals) : satsPerUnit = BigInt.from(10).pow(decimals); | ||
|
||
/// Obtains the number of satoshis from a string representation of this unit. | ||
/// | ||
/// Numbers must only contain digits and optionally one decimal point (".") in | ||
/// the event that there are any decimals. Ensure that there is at least one | ||
/// digit before and after the decimal point. There may only be decimals upto | ||
/// [decimals] in number. Zeros are striped from the left and stripped from | ||
/// the right after the decimal point. | ||
/// | ||
/// May throw [BadAmountString] if the number is not formatted correctly. | ||
BigInt toSats(String amount) { | ||
|
||
// Check format | ||
if (!_numberRegex.hasMatch(amount)) throw BadAmountString(); | ||
|
||
// Split decimal | ||
final split = amount.split("."); | ||
final includesPoint = split.length == 2; | ||
|
||
// Decimal places must not exceed expected decimals | ||
if (includesPoint && split[1].length > decimals) throw BadAmountString(); | ||
|
||
// Parse both sides into BigInt | ||
final left = BigInt.parse(split[0]); | ||
final right = includesPoint | ||
? BigInt.parse(split[1].padRight(decimals, "0")) | ||
: BigInt.zero; | ||
|
||
return left*satsPerUnit + right; | ||
|
||
} | ||
|
||
/// Obtains the string representation of the satoshis ([sats]) converted into | ||
/// this unit. | ||
String fromSats(BigInt sats) { | ||
|
||
final padded = sats.toString().padLeft(decimals+1, "0"); | ||
final insertIdx = padded.length-decimals; | ||
final left = padded.substring(0, insertIdx); | ||
final right = padded.substring(insertIdx); | ||
final withPoint = "$left.$right"; | ||
|
||
// Remove any trailing zeros and the decimal point if it comes before those | ||
// zeros | ||
return withPoint.replaceFirst(_trailZeroRegex, ""); | ||
|
||
} | ||
|
||
/// Represents a whole coin with 6 decimal places | ||
static final coin = CoinUnit(6); | ||
/// Represents a satoshi | ||
static final sats = CoinUnit(0); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import 'package:coinlib/coinlib.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
|
||
group("CoinUnit", () { | ||
|
||
void expectValidConversion( | ||
CoinUnit unit, String original, String sats, String result, | ||
) { | ||
final actualSats = unit.toSats(original); | ||
expect(actualSats, BigInt.parse(sats)); | ||
expect(unit.fromSats(actualSats), result); | ||
} | ||
|
||
void expectInvalid(CoinUnit unit, String str) => expect( | ||
() => unit.toSats(str), throwsA(isA<BadAmountString>()), | ||
); | ||
|
||
test("valid coin", () { | ||
|
||
void expectCoin(String original, String sats, String result) | ||
=> expectValidConversion(CoinUnit.coin, original, sats, result); | ||
|
||
expectCoin("0", "0", "0"); | ||
expectCoin("0.0", "0", "0"); | ||
expectCoin("0.000000", "0", "0"); | ||
expectCoin("000.000000", "0", "0"); | ||
|
||
expectCoin("1", "1000000", "1"); | ||
expectCoin("001", "1000000", "1"); | ||
expectCoin("1.000000", "1000000", "1"); | ||
|
||
expectCoin("1.123456", "1123456", "1.123456"); | ||
expectCoin("1.123", "1123000", "1.123"); | ||
expectCoin("1.123000", "1123000", "1.123"); | ||
expectCoin("0.000001", "1", "0.000001"); | ||
expectCoin("020.000001", "20000001", "20.000001"); | ||
|
||
}); | ||
|
||
test("valid sats", () { | ||
|
||
void expectSats(String original, String sats) | ||
=> expectValidConversion(CoinUnit.sats, original, sats, sats); | ||
|
||
expectSats("0", "0"); | ||
expectSats("000", "0"); | ||
expectSats("1", "1"); | ||
expectSats("00100", "100"); | ||
expectSats("1234567890", "1234567890"); | ||
expectSats("012345678090", "12345678090"); | ||
|
||
}); | ||
|
||
test("invalid coin", () { | ||
for (final invalid in [ | ||
"0.", ".123456", ".", "0.1.2", " 1", "1 ", "1 000", "1,000", "0.1234567", | ||
"1.1234560", "0a", "0A", "A0", "1/2", "one", "-1", "-0", | ||
]) { | ||
expectInvalid(CoinUnit.coin, invalid); | ||
} | ||
}); | ||
|
||
test("invalid sats", () { | ||
for (final invalid in [ | ||
"0.", ".123456", ".", "0.1", "0.1.2", " 1", "1 ", "1 000", "1,000", | ||
"0a", "0A", "A0", | ||
]) { | ||
expectInvalid(CoinUnit.sats, invalid); | ||
} | ||
}); | ||
|
||
}); | ||
|
||
} |