Natural language syntax for currencies (#440)

* Convert CurrencyUnit and LnCurrencyUnit Scalacheck to Scalatest

* Add Int and Long syntax to LnCurrencyUnit, MilliSatoshis and CurrencyUnit

* Tweak toString methods
This commit is contained in:
Torkel Rogstad 2019-05-02 13:49:35 +02:00 committed by Chris Stewart
parent 6944f948ab
commit e34c4505d4
8 changed files with 484 additions and 19 deletions

View file

@ -0,0 +1,167 @@
package org.bitcoins.core.currency
import org.bitcoins.core.currency._
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.core.gen.CurrencyUnitGenerator
import scala.util.Try
import org.bitcoins.core.number.Int64
import scala.util.Success
import scala.util.Failure
import org.scalacheck.Gen
import org.scalacheck.Shrink
class CurrencyUnitTest extends BitcoinSUnitTest {
behavior of "Satoshis"
it must "have symmetry serialization" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
assert(Satoshis(satoshis.hex) == satoshis)
}
}
it must "have Int syntax" in {
forAll(Gen.choose(0, Int.MaxValue)) { num =>
assert(num.bitcoins == Bitcoins(num))
assert(num.bitcoin == Bitcoins(num))
assert(num.BTC == Bitcoins(num))
assert(num.satoshis == Satoshis(Int64(num)))
assert(num.satoshi == Satoshis(Int64(num)))
assert(num.sats == Satoshis(Int64(num)))
assert(num.sat == Satoshis(Int64(num)))
}
}
it must "have Long syntax" in {
forAll(Gen.choose(0, Satoshis.max.toLong / 100000000)) { num =>
assert(num.bitcoins == Bitcoins(num))
assert(num.bitcoin == Bitcoins(num))
assert(num.BTC == Bitcoins(num))
}
forAll(Gen.choose(0, Satoshis.max.toLong)) { num =>
assert(num.satoshis == Satoshis(Int64(num)))
assert(num.satoshi == Satoshis(Int64(num)))
assert(num.sats == Satoshis(Int64(num)))
assert(num.sat == Satoshis(Int64(num)))
}
}
it must "have additive identity" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
assert(satoshis + CurrencyUnits.zero == satoshis)
}
}
it must "add satoshis" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1, num2) => (num1: Satoshis, num2: Satoshis) =>
val result: Try[Int64] = Try(Int64(num1.toBigInt + num2.toBigInt))
if (result.isSuccess && result.get >= Int64(Satoshis.min.toLong) &&
result.get <= Int64(Satoshis.max.toLong))
assert(num1 + num2 == Satoshis(result.get))
else assert(Try(num1 + num2).isFailure)
}
}
it must "have subtractive identity for satoshis" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
assert(satoshis - CurrencyUnits.zero == satoshis)
}
}
it must "subtract satoshis " in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1: Satoshis, num2: Satoshis) =>
val result: Try[Int64] = Try(Int64(num1.toBigInt - num2.toBigInt))
if (result.isSuccess && result.get >= Int64(Satoshis.min.toLong) &&
result.get <= Int64(Satoshis.max.toLong))
assert(num1 - num2 == Satoshis(result.get))
else assert(Try(num1 - num2).isFailure)
}
}
it must "multiply satoshis by zero" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
assert(satoshis * CurrencyUnits.zero == CurrencyUnits.zero)
}
}
it must "have multiplicative identity" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
assert(satoshis * Satoshis.one == satoshis)
}
}
it must "multiply satoshis" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1, num2) =>
val result: Try[Int64] = Try(Int64(num1.toBigInt * num2.toBigInt))
if (result.isSuccess && result.get >= Int64(Satoshis.min.toLong) &&
result.get <= Int64(Satoshis.max.toLong))
num1 * num2 == Satoshis(result.get)
else Try(num1 * num2).isFailure
}
}
private val satoshiWithInt: Gen[(Satoshis, Int)] = for {
sat <- CurrencyUnitGenerator.satoshis
num <- Gen.choose(Int.MinValue, Int.MaxValue)
} yield (sat, num)
it must "multiply a satoshi value with an int" in {
forAll(satoshiWithInt) {
case (sat, int) =>
val safeProduct = sat.multiplySafe(int)
val underlyingProduct = sat.toBigInt * int
if (underlyingProduct < Satoshis.max.toBigInt && underlyingProduct > Satoshis.min.toBigInt) {
assert(safeProduct.isSuccess)
assert(safeProduct.get.satoshis.toBigInt == underlyingProduct)
} else {
assert(safeProduct.isFailure)
}
}
}
it must "have '< & >=' property" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1, num2) =>
assert((num1 < num2) || (num1 >= num2))
}
}
it must "have '<= & >' property" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1, num2) =>
assert((num1 <= num2) || (num1 > num2))
}
}
it must "have '== & !=' property" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.satoshis) {
(num1, num2) =>
assert((num1 == num2) || (num1 != num2))
}
}
it must "convert satoshis to bitcoin and then back to satoshis" in {
forAll(CurrencyUnitGenerator.satoshis) { satoshis =>
val b = Bitcoins(satoshis)
assert(b.satoshis == satoshis)
}
}
it must "be able to add two unique currency unit types" in {
forAll(CurrencyUnitGenerator.satoshis, CurrencyUnitGenerator.bitcoins) {
(sats: Satoshis, btc: Bitcoins) =>
val result =
Try(Satoshis(Int64(sats.toBigInt + btc.satoshis.toBigInt)))
val expected = result.map(Bitcoins(_))
val actual: Try[CurrencyUnit] = Try(sats + btc)
if (actual.isSuccess && expected.isSuccess) actual.get == expected.get
else actual.isFailure && expected.isFailure
}
}
}

View file

@ -1,10 +1,166 @@
package org.bitcoins.core.protocol.ln.currency
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.ln.LnPolicy
import org.scalatest.{FlatSpec, MustMatchers}
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.core.gen.ln.LnCurrencyUnitGen
import scala.util.Success
import scala.util.Failure
import scala.util.Try
import org.scalacheck.Gen
import org.bitcoins.core.number.Int64
class LnCurrencyUnitTest extends BitcoinSUnitTest {
it must "have additive identity" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit) { lnUnit =>
assert(lnUnit + LnCurrencyUnits.zero == lnUnit)
}
}
it must "add to LnCurrencyUnits" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
val resultT: Try[LnCurrencyUnit] = Try(num1 + num2)
resultT match {
case Success(result) =>
assert(PicoBitcoins.min <= result)
assert(result <= PicoBitcoins.max)
assert(num1 + num2 == result)
case Failure(exc) => succeed
}
}
}
it must "have subtractive identity for LnCurrencyUnits" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit) { lnUnit =>
assert(lnUnit - LnCurrencyUnits.zero == lnUnit)
}
}
it must "subtract two LnCurrencyUnit values" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
val resultT: Try[LnCurrencyUnit] = Try(num1 - num2)
resultT match {
case Success(result) =>
assert(PicoBitcoins.min <= result)
assert(result <= PicoBitcoins.max)
assert(num1 - num2 == result)
case Failure(exc) => succeed
}
}
}
it must "multiply LnCurrencyUnit by zero" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit) { lnUnit =>
assert(lnUnit * LnCurrencyUnits.zero == LnCurrencyUnits.zero)
}
}
it must "have multiplicative identity for LnCurrencyUnits" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit) { lnUnit =>
assert(lnUnit * PicoBitcoins.one == lnUnit)
}
}
it must "multiply two LnCurrencyUnit values" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
val resultT: Try[LnCurrencyUnit] = Try(num1 * num2)
resultT match {
case Success(result) =>
assert(result >= PicoBitcoins.min)
assert(result <= PicoBitcoins.max)
assert(num1 * num2 == result)
case Failure(exc) => succeed
}
}
}
private val lnCurrWithInt: Gen[(LnCurrencyUnit, Int)] = for {
ln <- LnCurrencyUnitGen.lnCurrencyUnit
num <- Gen.choose(Int.MinValue, Int.MaxValue)
} yield (ln, num)
it must "multiply a LnCurrencyUnit value with an int" in {
forAll(lnCurrWithInt) {
case (ln, int) =>
val safeProduct = ln.multiplySafe(int)
val underlyingProduct = ln.toBigInt * int
if (underlyingProduct <= PicoBitcoins.min.toBigInt && underlyingProduct >= PicoBitcoins.max.toBigInt) {
assert(safeProduct.isSuccess)
safeProduct.get.toBigInt == underlyingProduct
} else {
safeProduct.isFailure
}
}
}
it must "have property '< & >=''" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
assert((num1 < num2) || (num1 >= num2))
}
}
it must "have property '<= & >'" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
assert((num1 <= num2) || (num1 > num2))
}
}
it must "have property '== & !='" in {
forAll(LnCurrencyUnitGen.lnCurrencyUnit, LnCurrencyUnitGen.lnCurrencyUnit) {
(num1, num2) =>
assert((num1 == num2) || (num1 != num2))
}
}
it must "have Int syntax" in {
forAll(Gen.choose(Int.MinValue, Int.MaxValue)) { num =>
assert(num.millibitcoins == MilliBitcoins(num))
assert(num.millibitcoin == MilliBitcoins(num))
assert(num.mBTC == MilliBitcoins(num))
assert(num.microbitcoins == MicroBitcoins(num))
assert(num.microbitcoin == MicroBitcoins(num))
assert(num.uBTC == MicroBitcoins(num))
assert(num.nanobitcoins == NanoBitcoins(num))
assert(num.nanobitcoin == NanoBitcoins(num))
assert(num.nBTC == NanoBitcoins(num))
assert(num.picobitcoins == PicoBitcoins(num))
assert(num.picobitcoin == PicoBitcoins(num))
assert(num.pBTC == PicoBitcoins(num))
}
}
it must "have Long syntax" in {
forAll(
Gen.choose(LnPolicy.minMilliBitcoins.toLong,
LnPolicy.maxMilliBitcoins.toLong)) { num =>
assert(num.millibitcoins == MilliBitcoins(num))
assert(num.millibitcoin == MilliBitcoins(num))
assert(num.mBTC == MilliBitcoins(num))
assert(num.microbitcoins == MicroBitcoins(num))
assert(num.microbitcoin == MicroBitcoins(num))
assert(num.uBTC == MicroBitcoins(num))
assert(num.nanobitcoins == NanoBitcoins(num))
assert(num.nanobitcoin == NanoBitcoins(num))
assert(num.nBTC == NanoBitcoins(num))
assert(num.picobitcoins == PicoBitcoins(num))
assert(num.picobitcoin == PicoBitcoins(num))
assert(num.pBTC == PicoBitcoins(num))
}
}
class LnCurrencyUnitTest extends FlatSpec with MustMatchers {
it must "serialize MilliBitcoins to string" in {
val milliBitcoins = MilliBitcoins(1000)
milliBitcoins.toEncodedString must be("1000m")

View file

@ -4,6 +4,7 @@ import org.bitcoins.testkit.core.gen.{CurrencyUnitGenerator, NumberGenerator}
import org.bitcoins.testkit.core.gen.ln.LnCurrencyUnitGen
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalatest.prop.PropertyChecks
import org.scalacheck.Gen
class MilliSatoshisTest extends BitcoinSUnitTest {
behavior of "MilliSatoshis"
@ -26,7 +27,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "add millisatoshis" in {
PropertyChecks.forAll(LnCurrencyUnitGen.milliSatoshisPair) {
forAll(LnCurrencyUnitGen.milliSatoshisPair) {
case (first, second) =>
val bigInt = first.toBigInt + second.toBigInt
assert((first + second).toBigInt == bigInt)
@ -39,7 +40,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
} yield (msat, num)
it must "multiply millisatoshis with an int" in {
PropertyChecks.forAll(msatWithNum) {
forAll(msatWithNum) {
case (msat, bigint) =>
val underlyingCalc = msat.toBigInt * bigint
assert((msat * bigint).toBigInt == underlyingCalc)
@ -47,7 +48,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "multiply millisatoshis with itself" in {
PropertyChecks.forAll(LnCurrencyUnitGen.milliSatoshisPair) {
forAll(LnCurrencyUnitGen.milliSatoshisPair) {
case (first, second) =>
val safe = first.multiplySafe(second)
val unsafe = first * second
@ -60,7 +61,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "subtract msats after adding them" in {
PropertyChecks.forAll(LnCurrencyUnitGen.milliSatoshisPair) {
forAll(LnCurrencyUnitGen.milliSatoshisPair) {
case (first, second) =>
val added = first + second
val subtracted = added - second
@ -69,7 +70,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "subtract msats" in {
PropertyChecks.forAll(LnCurrencyUnitGen.milliSatoshisPair) {
forAll(LnCurrencyUnitGen.milliSatoshisPair) {
case (first, second) =>
val subtracted = first subtractSafe second
val isPositive = (first.toBigInt - second.toBigInt) >= 0
@ -83,8 +84,7 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "covert from a ln currency unit -> millisatoshis -> lnCurrencyUnit" in {
PropertyChecks.forAll(LnCurrencyUnitGen.positivePicoBitcoin) { pb =>
forAll(LnCurrencyUnitGen.positivePicoBitcoin) { pb =>
val underlying = pb.toBigInt
//we lose the last digit of precision converting
//PicoBitcoins -> MilliSatoshis
@ -103,10 +103,28 @@ class MilliSatoshisTest extends BitcoinSUnitTest {
}
it must "convert sat -> msat -> sat" in {
PropertyChecks.forAll(CurrencyUnitGenerator.positiveRealistic) { sat =>
forAll(CurrencyUnitGenerator.positiveRealistic) { sat =>
val msat = MilliSatoshis(sat)
assert(msat.toSatoshis == sat)
}
}
it must "have Int syntax" in {
forAll(Gen.choose(0, Int.MaxValue)) { num =>
assert(num.millisatoshis == MilliSatoshis(num))
assert(num.millisatoshi == MilliSatoshis(num))
assert(num.msats == MilliSatoshis(num))
assert(num.msat == MilliSatoshis(num))
}
}
it must "have Long syntax" in {
forAll(Gen.choose(0L, Long.MaxValue)) { num =>
assert(num.millisatoshis == MilliSatoshis(num))
assert(num.millisatoshi == MilliSatoshis(num))
assert(num.msats == MilliSatoshis(num))
assert(num.msat == MilliSatoshis(num))
}
}
}

View file

@ -64,7 +64,11 @@ sealed abstract class CurrencyUnit
sealed abstract class Satoshis extends CurrencyUnit {
override type A = Int64
override def toString: String = s"$toLong sat"
override def toString: String = {
val num = toLong
val postFix = if (num == 1) "sat" else "sats"
s"$num $postFix"
}
override def bytes: ByteVector = RawSatoshisSerializer.write(this)

View file

@ -0,0 +1,40 @@
package org.bitcoins.core
import org.bitcoins.core.number.Int64
// We extend AnyVal to avoid runtime allocation of new
// objects. See the Scala documentation on value classes
// and universal traits for more:
// https://docs.scala-lang.org/overviews/core/value-classes.html
package object currency {
/** Provides natural language syntax for bitcoins */
implicit class BitcoinsInt(private val i: Int) extends AnyVal {
def bitcoins: Bitcoins = Bitcoins(i)
def bitcoin: Bitcoins = bitcoins
def BTC: Bitcoins = bitcoins
}
/** Provides natural language syntax for bitcoins */
implicit class BitcoinsLong(private val i: Long) extends AnyVal {
def bitcoins: Bitcoins = Bitcoins(i)
def bitcoin: Bitcoins = bitcoins
def BTC: Bitcoins = bitcoins
}
/** Provides natural language syntax for satoshis */
implicit class SatoshisInt(private val i: Int) extends AnyVal {
def satoshis: Satoshis = Satoshis(Int64(i))
def satoshi: Satoshis = satoshis
def sats: Satoshis = satoshis
def sat: Satoshis = satoshis
}
/** Provides natural language syntax for satoshis */
implicit class SatoshisLong(private val i: Long) extends AnyVal {
def satoshis: Satoshis = Satoshis(Int64(i))
def satoshi: Satoshis = satoshis
def sats: Satoshis = satoshis
def sat: Satoshis = satoshis
}
}

View file

@ -99,6 +99,8 @@ sealed abstract class LnCurrencyUnit
def toEncodedString: String = {
toBigInt + character.toString
}
override def toString(): String = s"$underlying ${character}BTC"
}
sealed abstract class MilliBitcoins extends LnCurrencyUnit {
@ -128,8 +130,6 @@ object MilliBitcoins extends BaseNumbers[MilliBitcoins] {
require(underlying <= LnPolicy.maxMilliBitcoins,
"Number was too big for MilliBitcoins, got: " + underlying)
override def toString: String =
s"${underlying / toPicoBitcoinMultiplier} mBTC"
}
}
@ -160,8 +160,6 @@ object MicroBitcoins extends BaseNumbers[MicroBitcoins] {
require(underlying <= LnPolicy.maxMicroBitcoins,
"Number was too big for MicroBitcoins, got: " + underlying)
override def toString: String =
s"${underlying / toPicoBitcoinMultiplier} uBTC"
}
}
@ -191,8 +189,6 @@ object NanoBitcoins extends BaseNumbers[NanoBitcoins] {
require(underlying <= LnPolicy.maxNanoBitcoins,
"Number was too big for NanoBitcoins, got: " + underlying)
override def toString: String =
s"${underlying / toPicoBitcoinMultiplier} nBTC"
}
}
@ -220,7 +216,6 @@ object PicoBitcoins extends BaseNumbers[PicoBitcoins] {
require(underlying <= LnPolicy.maxPicoBitcoins,
"Number was too big for PicoBitcoins, got: " + underlying)
override def toString: String = s"$toBigInt pBTC"
}
}

View file

@ -27,7 +27,11 @@ sealed abstract class MilliSatoshis
* 10 msat
* }}}
*/
override def toString: String = s"$toBigInt msat"
override def toString: String = {
val num = toBigInt
val postFix = if (num == 1) "msat" else "msats"
s"$num $postFix"
}
def toBigInt: BigInt = underlying

View file

@ -0,0 +1,81 @@
package org.bitcoins.core.protocol.ln
// We extend AnyVal to avoid runtime allocation of new
// objects. See the Scala documentation on value classes
// and universal traits for more:
// https://docs.scala-lang.org/overviews/core/value-classes.html
package object currency {
/** Provides natural language syntax for millisatoshis */
implicit class MilliSatoshisInt(private val i: Int) extends AnyVal {
def millisatoshis: MilliSatoshis = MilliSatoshis(i)
def millisatoshi: MilliSatoshis = millisatoshis
def msats: MilliSatoshis = millisatoshis
def msat: MilliSatoshis = millisatoshis
}
/** Provides natural language syntax for millisatoshis */
implicit class MilliSatoshisLong(private val i: Long) extends AnyVal {
def millisatoshis: MilliSatoshis = MilliSatoshis(i)
def millisatoshi: MilliSatoshis = millisatoshis
def msats: MilliSatoshis = millisatoshis
def msat: MilliSatoshis = millisatoshis
}
/** Provides natural language syntax for millibitcoins */
implicit class MilliBitcoinsInt(private val i: Int) extends AnyVal {
def millibitcoins: MilliBitcoins = MilliBitcoins(i)
def millibitcoin: MilliBitcoins = millibitcoins
def mBTC: MilliBitcoins = millibitcoins
}
/** Provides natural language syntax for millibitcoins */
implicit class MilliBitcoinsLong(private val i: Long) extends AnyVal {
def millibitcoins: MilliBitcoins = MilliBitcoins(i)
def millibitcoin: MilliBitcoins = millibitcoins
def mBTC: MilliBitcoins = millibitcoins
}
/** Provides natural language syntax for microbitcoins */
implicit class MicroBitcoinsInt(private val i: Int) extends AnyVal {
def microbitcoins: MicroBitcoins = MicroBitcoins(i)
def microbitcoin: MicroBitcoins = microbitcoins
def uBTC: MicroBitcoins = microbitcoins
}
/** Provides natural language syntax for microbitcoins */
implicit class MicroBitcoinsLong(private val i: Long) extends AnyVal {
def microbitcoins: MicroBitcoins = MicroBitcoins(i)
def microbitcoin: MicroBitcoins = microbitcoins
def uBTC: MicroBitcoins = microbitcoins
}
/** Provides natural language syntax for nanobitcoins */
implicit class NanoBitcoinsInt(private val i: Int) extends AnyVal {
def nanobitcoins: NanoBitcoins = NanoBitcoins(i)
def nanobitcoin: NanoBitcoins = nanobitcoins
def nBTC: NanoBitcoins = nanobitcoins
}
/** Provides natural language syntax for nanobitcoins */
implicit class NanoBitcoinsLong(private val i: Long) extends AnyVal {
def nanobitcoins: NanoBitcoins = NanoBitcoins(i)
def nanobitcoin: NanoBitcoins = nanobitcoins
def nBTC: NanoBitcoins = nanobitcoins
}
/** Provides natural language syntax for picobitcoins */
implicit class PicoitcoinsInt(private val i: Int) extends AnyVal {
def picobitcoins: PicoBitcoins = PicoBitcoins(i)
def picobitcoin: PicoBitcoins = picobitcoins
def pBTC: PicoBitcoins = picobitcoins
}
/** Provides natural language syntax for picobitcoins */
implicit class PicoitcoinsLong(private val i: Long) extends AnyVal {
def picobitcoins: PicoBitcoins = PicoBitcoins(i)
def picobitcoin: PicoBitcoins = picobitcoins
def pBTC: PicoBitcoins = picobitcoins
}
}