Create FeeUnitFactory and functions to calculate tx fee rates (#1990)

* Create FeeUnitFactory and functions to calculate tx fee rates

* Remove useless override, use factory

* doc

* Scaladocs + use vals

* Fix scaleFactor being set as default value
This commit is contained in:
Ben Carman 2020-09-10 06:45:25 -05:00 committed by GitHub
parent 74807e867b
commit e2163c99ff
4 changed files with 176 additions and 30 deletions

View File

@ -1,11 +1,7 @@
package org.bitcoins.core.wallet.fee
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.transaction.{
BaseTransaction,
Transaction,
WitnessTransaction
}
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.testkit.util.BitcoinSUnitTest
class FeeUnitTest extends BitcoinSUnitTest {
@ -13,6 +9,10 @@ class FeeUnitTest extends BitcoinSUnitTest {
val tx: Transaction = Transaction(
"02000000000101a2619b5d58b209439c937e563018efcf174063ca011e4f177a5b14e5ba76211c0100000017160014614e9b96cbc7477eda98f0936385ded6b636f74efeffffff024e3f57c4000000001600147cf00288c2c1b3c5cdf275db532a1c15c514bb2fae1112000000000016001440efb02597b9e9d9bc968f12cec3347e2e264c570247304402205768c2ac8178539fd44721e2a7541bedd6b55654f095143514624203c133f7e8022060d51f33fc2b5c1f51f26c7f703de21be6246dbb5fb7e1c6919aae6d442610c6012102b99a63f166ef53ca67a5c55ae969e80c33456e07189f8457e3438f000be42c19307d1900")
// testnet tx d34888da65ece048d41e0c74a59f225bbe7326e5f8c7437a4bf4df487822b3d3
val wtx: WitnessTransaction = WitnessTransaction(
"02000000000101a2619b5d58b209439c937e563018efcf174063ca011e4f177a5b14e5ba76211c0100000017160014614e9b96cbc7477eda98f0936385ded6b636f74efeffffff024e3f57c4000000001600147cf00288c2c1b3c5cdf275db532a1c15c514bb2fae1112000000000016001440efb02597b9e9d9bc968f12cec3347e2e264c570247304402205768c2ac8178539fd44721e2a7541bedd6b55654f095143514624203c133f7e8022060d51f33fc2b5c1f51f26c7f703de21be6246dbb5fb7e1c6919aae6d442610c6012102b99a63f166ef53ca67a5c55ae969e80c33456e07189f8457e3438f000be42c19307d1900")
it must "calculate the correct fee with a SatoshisPerByte fee rate" in {
val feeRate = SatoshisPerByte(Satoshis(3))
@ -47,9 +47,6 @@ class FeeUnitTest extends BitcoinSUnitTest {
}
it must "NOT have symmetry for SatoshisPerByte and SatoshisPerVirtualByte with WitnessTransactions" in {
val wtx = WitnessTransaction(
"02000000000101a2619b5d58b209439c937e563018efcf174063ca011e4f177a5b14e5ba76211c0100000017160014614e9b96cbc7477eda98f0936385ded6b636f74efeffffff024e3f57c4000000001600147cf00288c2c1b3c5cdf275db532a1c15c514bb2fae1112000000000016001440efb02597b9e9d9bc968f12cec3347e2e264c570247304402205768c2ac8178539fd44721e2a7541bedd6b55654f095143514624203c133f7e8022060d51f33fc2b5c1f51f26c7f703de21be6246dbb5fb7e1c6919aae6d442610c6012102b99a63f166ef53ca67a5c55ae969e80c33456e07189f8457e3438f000be42c19307d1900")
val satoshisPerByte = SatoshisPerByte(Satoshis(3))
val satoshisPerVByte = SatoshisPerVirtualByte(Satoshis(3))
@ -67,4 +64,62 @@ class FeeUnitTest extends BitcoinSUnitTest {
assert(satPerKb.toSatPerByte == SatoshisPerByte(Satoshis(3)))
}
it must "have matching scaleFactor between class and factory" in {
assert(
SatoshisPerKiloByte.zero.scaleFactor == SatoshisPerKiloByte.scaleFactor)
assert(SatoshisPerKW.zero.scaleFactor == SatoshisPerKW.scaleFactor)
assert(SatoshisPerByte.zero.scaleFactor == SatoshisPerByte.scaleFactor)
assert(
SatoshisPerVirtualByte.zero.scaleFactor == SatoshisPerVirtualByte.scaleFactor)
}
it must "have matching txSizeForCalc between class and factory" in {
assert(
SatoshisPerKiloByte.zero.txSizeForCalc(wtx) == SatoshisPerKiloByte
.txSizeForCalc(wtx))
assert(
SatoshisPerKW.zero.txSizeForCalc(wtx) == SatoshisPerKW.txSizeForCalc(wtx))
assert(
SatoshisPerByte.zero.txSizeForCalc(wtx) == SatoshisPerByte.txSizeForCalc(
wtx))
assert(
SatoshisPerVirtualByte.zero.txSizeForCalc(wtx) == SatoshisPerVirtualByte
.txSizeForCalc(wtx))
}
it must "correctly calculate the fee rate for the tx in SatoshisPerVirtualByte" in {
val inputAmount = Bitcoins(32.95260479)
assert(
SatoshisPerVirtualByte.calc(inputAmount, wtx) == SatoshisPerVirtualByte
.fromLong(147))
}
it must "correctly calculate the fee rate for the tx in SatoshisPerByte" in {
val inputAmount = Bitcoins(32.95260479)
assert(
SatoshisPerByte.calc(inputAmount, wtx) == SatoshisPerByte
.fromLong(98))
}
it must "correctly calculate the fee rate for the tx in SatoshisPerKW" in {
val inputAmount = Bitcoins(32.95260479)
assert(
SatoshisPerKW.calc(inputAmount, wtx) == SatoshisPerKW
.fromLong(36954))
}
it must "correctly calculate the fee rate for the tx in SatoshisPerKiloByte" in {
val inputAmount = Bitcoins(32.95260479)
assert(
SatoshisPerKiloByte.calc(inputAmount, wtx) == SatoshisPerKiloByte
.fromLong(98494))
}
}

View File

@ -39,16 +39,14 @@ object OutgoingTransactionDb {
s"sentAmount ($sentAmount) cannot be greater than the amount the wallet input ($inputAmount)")
val feePaid = inputAmount - totalOutput
// Calculate fee rate in satoshis per byte
val feeRate = feePaid.satoshis.toLong / tx.byteSize.toDouble
val feeRate = SatoshisPerByte.calc(inputAmount, tx)
OutgoingTransactionDb(
tx.txIdBE,
inputAmount,
sentAmount,
feePaid,
expectedFee,
SatoshisPerByte.fromLong(Math.round(feeRate))
feeRate
)
}
}

View File

@ -1,5 +1,6 @@
package org.bitcoins.core.protocol.transaction
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.protocol.script.{EmptyScriptWitness, ScriptWitness}
import org.bitcoins.core.util.BytesUtil
@ -103,6 +104,8 @@ sealed abstract class Transaction extends NetworkElement {
wtx.witness)
}
}
lazy val totalOutput: CurrencyUnit = outputs.map(_.value).sum
}
object Transaction extends Factory[Transaction] {

View File

@ -15,6 +15,28 @@ sealed abstract class FeeUnit {
final def *(long: Long): CurrencyUnit = Satoshis(toLong * long / scaleFactor)
def *(tx: Transaction): CurrencyUnit = calc(tx)
def factory: FeeUnitFactory[FeeUnit]
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
def scaleFactor: Long = factory.scaleFactor
require(scaleFactor > 0,
s"Scale factor cannot be less than or equal to 0, got $scaleFactor")
/** Takes the given transaction returns a size that will be used for calculating the fee rate.
* This is generally the denominator in the unit, ie sats/byte
*/
def txSizeForCalc(tx: Transaction): Long = factory.txSizeForCalc(tx)
/** Calculates the fee for the transaction using this fee rate, rounds down satoshis */
final def calc(tx: Transaction): CurrencyUnit = this * txSizeForCalc(tx)
def toLong: Long = currencyUnit.satoshis.toLong
}
trait FeeUnitFactory[+T <: FeeUnit] {
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
@ -28,9 +50,22 @@ sealed abstract class FeeUnit {
*/
def txSizeForCalc(tx: Transaction): Long
/** Calculates the fee for the transaction using this fee rate, rounds down satoshis */
final def calc(tx: Transaction): CurrencyUnit = this * txSizeForCalc(tx)
def toLong: Long = currencyUnit.satoshis.toLong
/** Creates an instance T where the value given is the numerator of the fee unit
*/
def fromLong(long: Long): T
/**
* Calculates the fee rate from the given Transaction.
* Uses Math.round for rounding up, which returns the closest long to the argument
* @param totalInputAmount Total amount being spent by the transaction's inputs
* @param tx Transaction to calculate the fee rate of
*/
def calc(totalInputAmount: CurrencyUnit, tx: Transaction): T = {
val feePaid = totalInputAmount - tx.totalOutput
val feeRate = feePaid.satoshis.toLong / txSizeForCalc(tx).toDouble
fromLong(Math.round(feeRate * scaleFactor))
}
}
/**
@ -41,17 +76,30 @@ sealed abstract class BitcoinFeeUnit extends FeeUnit
case class SatoshisPerByte(currencyUnit: CurrencyUnit) extends BitcoinFeeUnit {
def toSatPerKb: SatoshisPerKiloByte = {
lazy val toSatPerKb: SatoshisPerKiloByte = {
SatoshisPerKiloByte(currencyUnit.satoshis * Satoshis(1000))
}
override def txSizeForCalc(tx: Transaction): Long = tx.byteSize
override def scaleFactor: Long = 1
override def factory: FeeUnitFactory[SatoshisPerByte] = SatoshisPerByte
}
object SatoshisPerByte {
def fromLong(sats: Long): SatoshisPerByte = SatoshisPerByte(Satoshis(sats))
object SatoshisPerByte extends FeeUnitFactory[SatoshisPerByte] {
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
override lazy val scaleFactor: Long = 1
/** Takes the given transaction returns a size that will be used for calculating the fee rate.
* This is generally the denominator in the unit, ie sats/byte
*/
override def txSizeForCalc(tx: Transaction): Long = tx.byteSize
override def fromLong(sats: Long): SatoshisPerByte =
SatoshisPerByte(Satoshis(sats))
val zero: SatoshisPerByte = SatoshisPerByte(Satoshis.zero)
val one: SatoshisPerByte = SatoshisPerByte(Satoshis.one)
}
/**
@ -80,9 +128,27 @@ case class SatoshisPerKiloByte(currencyUnit: CurrencyUnit)
lazy val toSatPerByte: SatoshisPerByte = toSatPerByteExact
override def factory: FeeUnitFactory[SatoshisPerKiloByte] =
SatoshisPerKiloByte
}
object SatoshisPerKiloByte extends FeeUnitFactory[SatoshisPerKiloByte] {
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
override lazy val scaleFactor: Long = 1000
/** Takes the given transaction returns a size that will be used for calculating the fee rate.
* This is generally the denominator in the unit, ie sats/byte
*/
override def txSizeForCalc(tx: Transaction): Long = tx.byteSize
override def scaleFactor: Long = 1000
override def fromLong(sats: Long): SatoshisPerKiloByte =
SatoshisPerKiloByte(Satoshis(sats))
val zero: SatoshisPerKiloByte = SatoshisPerKiloByte(Satoshis.zero)
val one: SatoshisPerKiloByte = SatoshisPerKiloByte(Satoshis.one)
}
/**
@ -93,12 +159,25 @@ case class SatoshisPerKiloByte(currencyUnit: CurrencyUnit)
*/
case class SatoshisPerVirtualByte(currencyUnit: CurrencyUnit)
extends BitcoinFeeUnit {
override def txSizeForCalc(tx: Transaction): Long = tx.vsize
override def scaleFactor: Long = 1
override def factory: FeeUnitFactory[SatoshisPerVirtualByte] =
SatoshisPerVirtualByte
}
object SatoshisPerVirtualByte {
object SatoshisPerVirtualByte extends FeeUnitFactory[SatoshisPerVirtualByte] {
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
override lazy val scaleFactor: Long = 1
/** Takes the given transaction returns a size that will be used for calculating the fee rate.
* This is generally the denominator in the unit, ie sats/byte
*/
override def txSizeForCalc(tx: Transaction): Long = tx.vsize
override def fromLong(sats: Long): SatoshisPerVirtualByte =
SatoshisPerVirtualByte(Satoshis(sats))
val zero: SatoshisPerVirtualByte = SatoshisPerVirtualByte(CurrencyUnits.zero)
val one: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis.one)
@ -117,12 +196,23 @@ object SatoshisPerVirtualByte {
*/
case class SatoshisPerKW(currencyUnit: CurrencyUnit) extends BitcoinFeeUnit {
override def txSizeForCalc(tx: Transaction): Long = tx.weight
override def scaleFactor: Long = 1000
override def factory: FeeUnitFactory[SatoshisPerKW] = SatoshisPerKW
}
object SatoshisPerKW {
object SatoshisPerKW extends FeeUnitFactory[SatoshisPerKW] {
/** The coefficient the denominator in the unit is multiplied by,
* for example sats/kilobyte -> 1000
*/
override lazy val scaleFactor: Long = 1000
/** Takes the given transaction returns a size that will be used for calculating the fee rate.
* This is generally the denominator in the unit, ie sats/byte
*/
override def txSizeForCalc(tx: Transaction): Long = tx.weight
override def fromLong(sats: Long): SatoshisPerKW =
SatoshisPerKW(Satoshis(sats))
val zero: SatoshisPerKW = SatoshisPerKW(CurrencyUnits.zero)
val one: SatoshisPerKW = SatoshisPerKW(Satoshis.one)