Skip to content

Commit

Permalink
Add more numeric utilities to MilliSatoshi (#1103)
Browse files Browse the repository at this point in the history
Add comparisons and postfix operators.
Update most of the codebase to leverage those.
  • Loading branch information
t-bast authored Aug 29, 2019
1 parent 8f7a415 commit 46e4873
Show file tree
Hide file tree
Showing 93 changed files with 1,799 additions and 1,616 deletions.
120 changes: 60 additions & 60 deletions eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
package fr.acinq.eclair

import java.text.{DecimalFormat, NumberFormat}

import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi}
import grizzled.slf4j.Logging

import scala.util.{Failure, Success, Try}

/**
* Internal UI utility class, useful for lossless conversion between BtcAmount.
* The issue being that Satoshi contains a Long amount and it can not be converted to MilliSatoshi without losing the decimal part.
*/
* Internal UI utility class, useful for lossless conversion between BtcAmount.
* The issue being that Satoshi contains a Long amount and it can not be converted to MilliSatoshi without losing the decimal part.
*/
private sealed trait BtcAmountGUILossless {
def amount_msat: Long
def unit: CoinUnit
Expand Down Expand Up @@ -124,15 +126,15 @@ object CoinUtils extends Logging {
}

/**
* Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if
* it has too many decimals because MilliSatoshi only accepts Long amount.
*
* @param amount numeric String, can be decimal.
* @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC.
* @return amount as a MilliSatoshi object.
* @throws NumberFormatException if the amount parameter is not numeric.
* @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC.
*/
* Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if
* it has too many decimals because MilliSatoshi only accepts Long amount.
*
* @param amount numeric String, can be decimal.
* @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC.
* @return amount as a MilliSatoshi object.
* @throws NumberFormatException if the amount parameter is not numeric.
* @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC.
*/
@throws(classOf[NumberFormatException])
@throws(classOf[IllegalArgumentException])
def convertStringAmountToMsat(amount: String, unit: String): MilliSatoshi = {
Expand All @@ -152,13 +154,11 @@ object CoinUtils extends Logging {
}

def convertStringAmountToSat(amount: String, unit: String): Satoshi =
CoinUtils.convertStringAmountToMsat(amount, unit).truncateToSatoshi
CoinUtils.convertStringAmountToMsat(amount, unit).truncateToSatoshi

/**
* Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported.
* @param unit
* @return
*/
* Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported.
*/
def getUnitFromString(unit: String): CoinUnit = unit.toLowerCase() match {
case u if u == MSatUnit.code || u == MSatUnit.label.toLowerCase() => MSatUnit
case u if u == SatUnit.code || u == SatUnit.label.toLowerCase() => SatUnit
Expand All @@ -169,53 +169,53 @@ object CoinUtils extends Logging {
}

/**
* Converts BtcAmount to a GUI Unit (wrapper containing amount as a millisatoshi long)
*
* @param amount BtcAmount
* @param unit unit to convert to
* @return a GUICoinAmount
*/
* Converts BtcAmount to a GUI Unit (wrapper containing amount as a millisatoshi long)
*
* @param amount BtcAmount
* @param unit unit to convert to
* @return a GUICoinAmount
*/
private def convertAmountToGUIUnit(amount: BtcAmount, unit: CoinUnit): BtcAmountGUILossless = (amount, unit) match {
// amount is msat, so no conversion required
case (a: MilliSatoshi, MSatUnit) => GUIMSat(a.amount * MSatUnit.factorToMsat)
case (a: MilliSatoshi, SatUnit) => GUISat(a.amount * MSatUnit.factorToMsat)
case (a: MilliSatoshi, BitUnit) => GUIBits(a.amount * MSatUnit.factorToMsat)
case (a: MilliSatoshi, MBtcUnit) => GUIMBtc(a.amount * MSatUnit.factorToMsat)
case (a: MilliSatoshi, BtcUnit) => GUIBtc(a.amount * MSatUnit.factorToMsat)
case (a: MilliSatoshi, MSatUnit) => GUIMSat(a.toLong * MSatUnit.factorToMsat)
case (a: MilliSatoshi, SatUnit) => GUISat(a.toLong * MSatUnit.factorToMsat)
case (a: MilliSatoshi, BitUnit) => GUIBits(a.toLong * MSatUnit.factorToMsat)
case (a: MilliSatoshi, MBtcUnit) => GUIMBtc(a.toLong * MSatUnit.factorToMsat)
case (a: MilliSatoshi, BtcUnit) => GUIBtc(a.toLong * MSatUnit.factorToMsat)

// amount is satoshi, convert sat -> msat
case (a: Satoshi, MSatUnit) => GUIMSat(a.amount * SatUnit.factorToMsat)
case (a: Satoshi, SatUnit) => GUISat(a.amount * SatUnit.factorToMsat)
case (a: Satoshi, BitUnit) => GUIBits(a.amount * SatUnit.factorToMsat)
case (a: Satoshi, MBtcUnit) => GUIMBtc(a.amount * SatUnit.factorToMsat)
case (a: Satoshi, BtcUnit) => GUIBtc(a.amount * SatUnit.factorToMsat)
case (a: Satoshi, MSatUnit) => GUIMSat(a.toLong * SatUnit.factorToMsat)
case (a: Satoshi, SatUnit) => GUISat(a.toLong * SatUnit.factorToMsat)
case (a: Satoshi, BitUnit) => GUIBits(a.toLong * SatUnit.factorToMsat)
case (a: Satoshi, MBtcUnit) => GUIMBtc(a.toLong * SatUnit.factorToMsat)
case (a: Satoshi, BtcUnit) => GUIBtc(a.toLong * SatUnit.factorToMsat)

// amount is mbtc
case (a: MilliBtc, MSatUnit) => GUIMSat((a.amount * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, SatUnit) => GUISat((a.amount * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, BitUnit) => GUIBits((a.amount * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, MBtcUnit) => GUIMBtc((a.amount * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, BtcUnit) => GUIBtc((a.amount * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, MSatUnit) => GUIMSat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, SatUnit) => GUISat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, BitUnit) => GUIBits((a.toBigDecimal * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, MBtcUnit) => GUIMBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong)
case (a: MilliBtc, BtcUnit) => GUIBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong)

// amount is mbtc
case (a: Btc, MSatUnit) => GUIMSat((a.amount * BtcUnit.factorToMsat).toLong)
case (a: Btc, SatUnit) => GUISat((a.amount * BtcUnit.factorToMsat).toLong)
case (a: Btc, BitUnit) => GUIBits((a.amount * BtcUnit.factorToMsat).toLong)
case (a: Btc, MBtcUnit) => GUIMBtc((a.amount * BtcUnit.factorToMsat).toLong)
case (a: Btc, BtcUnit) => GUIBtc((a.amount * BtcUnit.factorToMsat).toLong)
case (a: Btc, MSatUnit) => GUIMSat((a.toBigDecimal * BtcUnit.factorToMsat).toLong)
case (a: Btc, SatUnit) => GUISat((a.toBigDecimal * BtcUnit.factorToMsat).toLong)
case (a: Btc, BitUnit) => GUIBits((a.toBigDecimal * BtcUnit.factorToMsat).toLong)
case (a: Btc, MBtcUnit) => GUIMBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong)
case (a: Btc, BtcUnit) => GUIBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong)

case (a, _) =>
case (_, _) =>
throw new IllegalArgumentException(s"unhandled conversion from $amount to $unit")
}

/**
* Converts the amount to the user preferred unit and returns a localized formatted String.
* This method is useful for read only displays.
*
* @param amount BtcAmount
* @param withUnit if true, append the user unit shortLabel (mBTC, BTC, mSat...)
* @return formatted amount
*/
* Converts the amount to the user preferred unit and returns a localized formatted String.
* This method is useful for read only displays.
*
* @param amount BtcAmount
* @param withUnit if true, append the user unit shortLabel (mBTC, BTC, mSat...)
* @return formatted amount
*/
def formatAmountInUnit(amount: BtcAmount, unit: CoinUnit, withUnit: Boolean = false): String = {
val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit))
if (withUnit) s"$formatted ${unit.shortLabel}" else formatted
Expand All @@ -227,14 +227,14 @@ object CoinUtils extends Logging {
}

/**
* Converts the amount to the user preferred unit and returns the BigDecimal value.
* This method is useful to feed numeric text input without formatting.
*
* Returns -1 if the given amount can not be converted.
*
* @param amount BtcAmount
* @return BigDecimal value of the BtcAmount
*/
* Converts the amount to the user preferred unit and returns the BigDecimal value.
* This method is useful to feed numeric text input without formatting.
*
* Returns -1 if the given amount can not be converted.
*
* @param amount BtcAmount
* @return BigDecimal value of the BtcAmount
*/
def rawAmountInUnit(amount: BtcAmount, unit: CoinUnit): BigDecimal = Try(convertAmountToGUIUnit(amount, unit) match {
case a: BtcAmountGUILossless => BigDecimal(a.amount_msat) / a.unit.factorToMsat
case a => throw new IllegalArgumentException(s"unhandled unit $a")
Expand All @@ -245,5 +245,5 @@ object CoinUtils extends Logging {
-1
}

def rawAmountInUnit(msat: MilliSatoshi, unit: CoinUnit): BigDecimal = BigDecimal(msat.amount) / unit.factorToMsat
def rawAmountInUnit(msat: MilliSatoshi, unit: CoinUnit): BigDecimal = BigDecimal(msat.toLong) / unit.factorToMsat
}
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.switchboard ? Peer.OpenChannel(
remoteNodeId = nodeId,
fundingSatoshis = fundingAmount,
pushMsat = pushAmount_opt.getOrElse(MilliSatoshi(0)),
pushMsat = pushAmount_opt.getOrElse(0 msat),
fundingTxFeeratePerKw_opt = fundingFeerateSatByte_opt.map(feerateByte2Kw),
channelFlags = flags_opt.map(_.toByte),
timeout_opt = Some(openTimeout))).mapTo[String]
Expand Down
71 changes: 71 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair

import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi}

/**
* Created by t-bast on 22/08/2019.
*/

/**
* One MilliSatoshi is a thousand of a Satoshi, the smallest unit usable in bitcoin
*/
case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatoshi] {

// @formatter:off
def +(other: MilliSatoshi) = MilliSatoshi(underlying + other.underlying)
def +(other: BtcAmount) = MilliSatoshi(underlying + other.toMilliSatoshi.underlying)
def -(other: MilliSatoshi) = MilliSatoshi(underlying - other.underlying)
def -(other: BtcAmount) = MilliSatoshi(underlying - other.toMilliSatoshi.underlying)
def *(m: Long) = MilliSatoshi(underlying * m)
def *(m: Double) = MilliSatoshi((underlying * m).toLong)
def /(d: Long) = MilliSatoshi(underlying / d)
def unary_-() = MilliSatoshi(-underlying)

override def compare(other: MilliSatoshi): Int = underlying.compareTo(other.underlying)
// Since BtcAmount is a sealed trait that MilliSatoshi cannot extend, we need to redefine comparison operators.
def compare(other: BtcAmount): Int = compare(other.toMilliSatoshi)
def <=(other: BtcAmount): Boolean = compare(other) <= 0
def >=(other: BtcAmount): Boolean = compare(other) >= 0
def <(other: BtcAmount): Boolean = compare(other) < 0
def >(other: BtcAmount): Boolean = compare(other) > 0

// We provide asymmetric min/max functions to provide more control on the return type.
def max(other: MilliSatoshi): MilliSatoshi = if (this > other) this else other
def max(other: BtcAmount): MilliSatoshi = if (this > other) this else other.toMilliSatoshi
def min(other: MilliSatoshi): MilliSatoshi = if (this < other) this else other
def min(other: BtcAmount): MilliSatoshi = if (this < other) this else other.toMilliSatoshi

def truncateToSatoshi: Satoshi = Satoshi(underlying / 1000)
def toLong: Long = underlying
override def toString = s"$underlying msat"
// @formatter:on

}

object MilliSatoshi {

private def satoshi2millisatoshi(input: Satoshi): MilliSatoshi = MilliSatoshi(input.toLong * 1000L)

def toMilliSatoshi(amount: BtcAmount): MilliSatoshi = amount match {
case sat: Satoshi => satoshi2millisatoshi(sat)
case millis: MilliBtc => satoshi2millisatoshi(millibtc2satoshi(millis))
case bitcoin: Btc => satoshi2millisatoshi(btc2satoshi(bitcoin))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ class UInt64Serializer extends CustomSerializer[UInt64](format => ({ null }, {
}))

class SatoshiSerializer extends CustomSerializer[Satoshi](format => ({ null }, {
case x: Satoshi => JInt(x.amount)
case x: Satoshi => JInt(x.toLong)
}))

class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({ null }, {
case x: MilliSatoshi => JInt(x.amount)
case x: MilliSatoshi => JInt(x.toLong)
}))

class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ({ null }, {
Expand Down Expand Up @@ -124,7 +124,7 @@ class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ({ n
}))

class InputInfoSerializer extends CustomSerializer[InputInfo](format => ({ null }, {
case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.amount)))
case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong)))
}))

class ColorSerializer extends CustomSerializer[Color](format => ({ null }, {
Expand Down
Loading

0 comments on commit 46e4873

Please sign in to comment.