Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more numeric utilities to MilliSatoshi #1103

Merged
merged 20 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}

araspitzu marked this conversation as resolved.
Show resolved Hide resolved
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
t-bast marked this conversation as resolved.
Show resolved Hide resolved
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