Modify estimatefee endpoint to return a number rather than a string (#3973)

* Modify estimatefee endpoint to return a number rather than a string

* Fix compile

* Fix infinite loop on converting to sats/vb, add tests
This commit is contained in:
Chris Stewart 2022-01-12 12:17:04 -06:00 committed by GitHub
parent 66e23b61a8
commit 9dd126bb9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 107 additions and 35 deletions

View File

@ -0,0 +1,41 @@
package org.bitcoins.server
import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.bitcoins.server.routes.ServerCommand
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.wallet.MockWalletApi
import org.scalamock.scalatest.MockFactory
import org.scalatest.wordspec.AnyWordSpec
import akka.http.scaladsl.model.ContentTypes._
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerVirtualByte}
import scala.concurrent.Future
class WalletRoutesSpec
extends AnyWordSpec
with ScalatestRouteTest
with MockFactory {
implicit val conf: BitcoinSAppConfig =
BitcoinSTestAppConfig.getSpvTestConfig()
val mockWalletApi = mock[MockWalletApi]
val walletRoutes: WalletRoutes =
WalletRoutes(mockWalletApi)(system, conf.walletConf)
"WalletRoutes" should {
"estimatefee" in {
(mockWalletApi.getFeeRate: () => Future[FeeUnit])
.expects()
.returning(Future.successful(SatoshisPerVirtualByte.one))
val route =
walletRoutes.handleCommand(ServerCommand("estimatefee", ujson.Arr()))
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == s"""{"result":1,"error":null}""")
}
}
}
}

View File

@ -810,8 +810,8 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
case ServerCommand("estimatefee", _) => case ServerCommand("estimatefee", _) =>
complete { complete {
wallet.getFeeRate.map { fee => wallet.getFeeRate().map { fee =>
Server.httpSuccess(fee.toString) Server.httpSuccess(fee.toSatsPerVByte)
} }
} }

View File

@ -83,7 +83,7 @@ class BitcoindRpcClient(override val instance: BitcoindInstance)(implicit
// Fee Rate Provider // Fee Rate Provider
override def getFeeRate: Future[FeeUnit] = override def getFeeRate(): Future[FeeUnit] =
estimateSmartFee(blocks = 6).flatMap { result => estimateSmartFee(blocks = 6).flatMap { result =>
result.feerate match { result.feerate match {
case Some(feeRate) => Future.successful(feeRate) case Some(feeRate) => Future.successful(feeRate)

View File

@ -65,10 +65,25 @@ class FeeUnitTest extends BitcoinSUnitTest {
assert(satPerKb.toSatPerByte == SatoshisPerByte(Satoshis(3))) assert(satPerKb.toSatPerByte == SatoshisPerByte(Satoshis(3)))
} }
it must "correctly convert SatoshisPerVirtualByte to SatoshisPerKW" in { it must "correctly convert SatoshisPerKiloByte to SatoshisPerVirtualByte and vice versa" in {
val satPerVb = SatoshisPerVirtualByte(Satoshis(3)) val satPerKb = SatoshisPerKiloByte(Satoshis(3000))
val expectedSatsPerVByte = SatoshisPerVirtualByte(Satoshis(3))
assert(satPerVb.toSatoshisPerKW == SatoshisPerKW(Satoshis(750))) assert(satPerKb.toSatsPerVByte == expectedSatsPerVByte)
}
it must "correctly convert SatoshisPerVirtualByte to SatoshisPerKW and vice versa" in {
val satPerVb = SatoshisPerVirtualByte(Satoshis(3))
val expectedSatsPerKW = SatoshisPerKW(Satoshis(750))
assert(satPerVb.toSatoshisPerKW == expectedSatsPerKW)
assert(expectedSatsPerKW.toSatsPerVByte == satPerVb)
}
it must "correctly convert SatoshisPerByte to SatoshisPerVirtualByte and vice versa" in {
val satsPerVb = SatoshisPerVirtualByte(Satoshis(100))
val expectedSatsPerByte = SatoshisPerByte(Satoshis(100))
assert(expectedSatsPerByte.toSatsPerVByte == satsPerVb)
} }
it must "calculate the same fee when using SatoshisPerVirtualByte.toSatoshisPerKW" in { it must "calculate the same fee when using SatoshisPerVirtualByte.toSatoshisPerKW" in {

View File

@ -6,6 +6,6 @@ import scala.concurrent.Future
trait FeeRateApi { trait FeeRateApi {
def getFeeRate: Future[FeeUnit] def getFeeRate(): Future[FeeUnit]
} }

View File

@ -40,7 +40,7 @@ trait WalletApi extends StartStopAsync[WalletApi] {
def broadcastTransaction(transaction: Transaction): Future[Unit] = def broadcastTransaction(transaction: Transaction): Future[Unit] =
nodeApi.broadcastTransaction(transaction) nodeApi.broadcastTransaction(transaction)
def getFeeRate: Future[FeeUnit] = feeRateApi.getFeeRate def getFeeRate(): Future[FeeUnit] = feeRateApi.getFeeRate()
def start(): Future[WalletApi] def start(): Future[WalletApi]
@ -239,7 +239,7 @@ trait WalletApi extends StartStopAsync[WalletApi] {
protected def determineFeeRate(feeRateOpt: Option[FeeUnit]): Future[FeeUnit] = protected def determineFeeRate(feeRateOpt: Option[FeeUnit]): Future[FeeUnit] =
feeRateOpt match { feeRateOpt match {
case None => case None =>
feeRateApi.getFeeRate feeRateApi.getFeeRate()
case Some(feeRate) => case Some(feeRate) =>
Future.successful(feeRate) Future.successful(feeRate)
} }

View File

@ -35,6 +35,9 @@ sealed abstract class FeeUnit {
def toLong: Long = currencyUnit.satoshis.toLong def toLong: Long = currencyUnit.satoshis.toLong
override def toString: String = s"$toLong ${factory.unitString}" override def toString: String = s"$toLong ${factory.unitString}"
/** Converts the current fee unit to sats/vybte */
def toSatsPerVByte: SatoshisPerVirtualByte
} }
trait FeeUnitFactory[+T <: FeeUnit] { trait FeeUnitFactory[+T <: FeeUnit] {
@ -84,6 +87,9 @@ case class SatoshisPerByte(currencyUnit: CurrencyUnit) extends BitcoinFeeUnit {
} }
override def factory: FeeUnitFactory[SatoshisPerByte] = SatoshisPerByte override def factory: FeeUnitFactory[SatoshisPerByte] = SatoshisPerByte
override val toSatsPerVByte: SatoshisPerVirtualByte = SatoshisPerVirtualByte(
currencyUnit)
} }
object SatoshisPerByte extends FeeUnitFactory[SatoshisPerByte] { object SatoshisPerByte extends FeeUnitFactory[SatoshisPerByte] {
@ -132,6 +138,10 @@ case class SatoshisPerKiloByte(currencyUnit: CurrencyUnit)
lazy val toSatPerByte: SatoshisPerByte = toSatPerByteExact lazy val toSatPerByte: SatoshisPerByte = toSatPerByteExact
/** Converts sats/kb -> sats/vbyte with rounding if necessary. */
override val toSatsPerVByte: SatoshisPerVirtualByte =
toSatPerByteRounded.toSatsPerVByte
override def factory: FeeUnitFactory[SatoshisPerKiloByte] = override def factory: FeeUnitFactory[SatoshisPerKiloByte] =
SatoshisPerKiloByte SatoshisPerKiloByte
@ -172,6 +182,9 @@ case class SatoshisPerVirtualByte(currencyUnit: CurrencyUnit)
override def toString: String = s"$toLong sats/vbyte" override def toString: String = s"$toLong sats/vbyte"
lazy val toSatoshisPerKW: SatoshisPerKW = SatoshisPerKW(currencyUnit * 250) lazy val toSatoshisPerKW: SatoshisPerKW = SatoshisPerKW(currencyUnit * 250)
override val toSatsPerVByte: SatoshisPerVirtualByte = this
} }
object SatoshisPerVirtualByte extends FeeUnitFactory[SatoshisPerVirtualByte] { object SatoshisPerVirtualByte extends FeeUnitFactory[SatoshisPerVirtualByte] {
@ -210,6 +223,9 @@ case class SatoshisPerKW(currencyUnit: CurrencyUnit) extends BitcoinFeeUnit {
override def factory: FeeUnitFactory[SatoshisPerKW] = SatoshisPerKW override def factory: FeeUnitFactory[SatoshisPerKW] = SatoshisPerKW
override def toString: String = s"$toLong sats/kw" override def toString: String = s"$toLong sats/kw"
override val toSatsPerVByte: SatoshisPerVirtualByte = SatoshisPerVirtualByte(
currencyUnit / Satoshis(250))
} }
object SatoshisPerKW extends FeeUnitFactory[SatoshisPerKW] { object SatoshisPerKW extends FeeUnitFactory[SatoshisPerKW] {

View File

@ -81,9 +81,9 @@ class FeeRateProviderTest extends BitcoinSAsyncTest {
it must "get a cached fee rate from a cachedHttpFeeRateProvider" in { it must "get a cached fee rate from a cachedHttpFeeRateProvider" in {
val provider = MempoolSpaceProvider(FastestFeeTarget, MainNet, proxyParams) val provider = MempoolSpaceProvider(FastestFeeTarget, MainNet, proxyParams)
for { for {
feeRate <- provider.getFeeRate feeRate <- provider.getFeeRate()
_ <- AsyncUtil.nonBlockingSleep(20.seconds) _ <- AsyncUtil.nonBlockingSleep(20.seconds)
cached <- provider.getFeeRate cached <- provider.getFeeRate()
} yield assert(feeRate == cached) } yield assert(feeRate == cached)
} }
@ -94,13 +94,13 @@ class FeeRateProviderTest extends BitcoinSAsyncTest {
it must "get the correct fee rate from a ConstantFeeRateProvider" in { it must "get the correct fee rate from a ConstantFeeRateProvider" in {
val provider = ConstantFeeRateProvider(SatoshisPerByte(Satoshis(4))) val provider = ConstantFeeRateProvider(SatoshisPerByte(Satoshis(4)))
provider.getFeeRate.map { feeRate => provider.getFeeRate().map { feeRate =>
assert(feeRate == SatoshisPerByte(Satoshis(4))) assert(feeRate == SatoshisPerByte(Satoshis(4)))
} }
} }
private def testProvider(provider: FeeRateApi): Future[Assertion] = { private def testProvider(provider: FeeRateApi): Future[Assertion] = {
provider.getFeeRate.map { feeRate => provider.getFeeRate().map { feeRate =>
assert(feeRate.toLong > 0) assert(feeRate.toLong > 0)
} }
} }

View File

@ -6,5 +6,5 @@ import org.bitcoins.core.wallet.fee.FeeUnit
import scala.concurrent.Future import scala.concurrent.Future
case class ConstantFeeRateProvider(feeUnit: FeeUnit) extends FeeRateApi { case class ConstantFeeRateProvider(feeUnit: FeeUnit) extends FeeRateApi {
def getFeeRate: Future[FeeUnit] = Future.successful(feeUnit) def getFeeRate(): Future[FeeUnit] = Future.successful(feeUnit)
} }

View File

@ -38,7 +38,7 @@ abstract class HttpFeeRateProvider[T <: FeeUnit] extends FeeRateApi {
protected def proxyParams: Option[Socks5ProxyParams] protected def proxyParams: Option[Socks5ProxyParams]
def getFeeRate: Future[T] = { override def getFeeRate(): Future[T] = {
HttpFeeRateProvider HttpFeeRateProvider
.makeApiCall(uri, proxyParams) .makeApiCall(uri, proxyParams)
.flatMap(ret => Future.fromTry(converter(ret)))(system.dispatcher) .flatMap(ret => Future.fromTry(converter(ret)))(system.dispatcher)
@ -54,13 +54,13 @@ abstract class CachedHttpFeeRateProvider[T <: FeeUnit]
private def updateFeeRate(): Future[T] = { private def updateFeeRate(): Future[T] = {
implicit val ec: ExecutionContextExecutor = system.dispatcher implicit val ec: ExecutionContextExecutor = system.dispatcher
super.getFeeRate.map { feeRate => super.getFeeRate().map { feeRate =>
cachedFeeRateOpt = Some((feeRate, TimeUtil.now)) cachedFeeRateOpt = Some((feeRate, TimeUtil.now))
feeRate feeRate
} }
} }
override def getFeeRate: Future[T] = { override def getFeeRate(): Future[T] = {
cachedFeeRateOpt match { cachedFeeRateOpt match {
case None => case None =>
updateFeeRate() updateFeeRate()

View File

@ -309,7 +309,7 @@ object BitcoinSWalletTest extends WalletLogger {
// Useful for tests // Useful for tests
var lastFeeRate: Option[FeeUnit] = None var lastFeeRate: Option[FeeUnit] = None
override def getFeeRate: Future[FeeUnit] = { override def getFeeRate(): Future[FeeUnit] = {
val feeRate = FeeUnitGen.feeUnit.sampleSome val feeRate = FeeUnitGen.feeUnit.sampleSome
lastFeeRate = Some(feeRate) lastFeeRate = Some(feeRate)

View File

@ -74,7 +74,7 @@ class AddressTagIntegrationTest extends BitcoinSWalletTest {
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind)) .map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
account <- wallet.getDefaultAccount() account <- wallet.getDefaultAccount()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
(txBuilder, utxoInfos) <- bitcoind.getNewAddress.flatMap { addr => (txBuilder, utxoInfos) <- bitcoind.getNewAddress.flatMap { addr =>
val output = TransactionOutput(valueToBitcoind, addr.scriptPubKey) val output = TransactionOutput(valueToBitcoind, addr.scriptPubKey)
wallet wallet

View File

@ -39,7 +39,7 @@ class FundTransactionHandlingTest
fundedWallet: WalletWithBitcoind => fundedWallet: WalletWithBitcoind =>
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
for { for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = fundedTx <- wallet.fundRawTransaction(destinations =
Vector(destination), Vector(destination),
feeRate = feeRate, feeRate = feeRate,
@ -60,7 +60,7 @@ class FundTransactionHandlingTest
val newDestination = destination.copy(value = amt) val newDestination = destination.copy(value = amt)
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
for { for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = fundedTx <- wallet.fundRawTransaction(destinations =
Vector(newDestination), Vector(newDestination),
feeRate = feeRate, feeRate = feeRate,
@ -81,7 +81,7 @@ class FundTransactionHandlingTest
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
for { for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = destinations, fundedTx <- wallet.fundRawTransaction(destinations = destinations,
feeRate = feeRate, feeRate = feeRate,
fromTagOpt = None, fromTagOpt = None,
@ -106,7 +106,7 @@ class FundTransactionHandlingTest
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
val fundedTxF = for { val fundedTxF = for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = fundedTx <- wallet.fundRawTransaction(destinations =
Vector(tooBigOutput), Vector(tooBigOutput),
feeRate = feeRate, feeRate = feeRate,
@ -127,7 +127,7 @@ class FundTransactionHandlingTest
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
val fundedTxF = for { val fundedTxF = for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = fundedTx <- wallet.fundRawTransaction(destinations =
Vector(tooBigOutput), Vector(tooBigOutput),
feeRate = feeRate, feeRate = feeRate,
@ -150,7 +150,7 @@ class FundTransactionHandlingTest
val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig) val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
val account1DbF = wallet.accountDAO.findByAccount(account1) val account1DbF = wallet.accountDAO.findByAccount(account1)
for { for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
account1DbOpt <- account1DbF account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination), fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate, feeRate,
@ -171,7 +171,7 @@ class FundTransactionHandlingTest
val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig) val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
val account1DbF = wallet.accountDAO.findByAccount(account1) val account1DbF = wallet.accountDAO.findByAccount(account1)
val fundedTxF = for { val fundedTxF = for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
account1DbOpt <- account1DbF account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination), fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate, feeRate,
@ -188,7 +188,7 @@ class FundTransactionHandlingTest
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
val bitcoind = fundedWallet.bitcoind val bitcoind = fundedWallet.bitcoind
val fundedTxF = for { val fundedTxF = for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
_ <- wallet.createNewAccount(wallet.keyManager.kmParams) _ <- wallet.createNewAccount(wallet.keyManager.kmParams)
accounts <- wallet.accountDAO.findAll() accounts <- wallet.accountDAO.findAll()
account2 = accounts.find(_.hdAccount.index == 2).get account2 = accounts.find(_.hdAccount.index == 2).get
@ -215,7 +215,7 @@ class FundTransactionHandlingTest
fundedWallet: WalletWithBitcoind => fundedWallet: WalletWithBitcoind =>
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
for { for {
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
fundedTx <- wallet.fundRawTransaction(destinations = fundedTx <- wallet.fundRawTransaction(destinations =
Vector(destination), Vector(destination),
feeRate = feeRate, feeRate = feeRate,
@ -235,7 +235,7 @@ class FundTransactionHandlingTest
tag: AddressTag): Future[Assertion] = { tag: AddressTag): Future[Assertion] = {
for { for {
account <- wallet.getDefaultAccount() account <- wallet.getDefaultAccount()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
taggedAddr <- wallet.getNewAddress(Vector(tag)) taggedAddr <- wallet.getNewAddress(Vector(tag))
_ <- _ <-
wallet.sendToAddress(taggedAddr, destination.value * 2, Some(feeRate)) wallet.sendToAddress(taggedAddr, destination.value * 2, Some(feeRate))

View File

@ -316,7 +316,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTestCachedBitcoindNewest {
for { for {
oldTransactions <- wallet.listTransactions() oldTransactions <- wallet.listTransactions()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
tx <- wallet.fundRawTransaction(Vector(dummyOutput), tx <- wallet.fundRawTransaction(Vector(dummyOutput),
feeRate, feeRate,
fromTagOpt = None, fromTagOpt = None,
@ -342,7 +342,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTestCachedBitcoindNewest {
for { for {
oldTransactions <- wallet.listTransactions() oldTransactions <- wallet.listTransactions()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
tx <- wallet.fundRawTransaction(Vector(dummyOutput), tx <- wallet.fundRawTransaction(Vector(dummyOutput),
feeRate, feeRate,
fromTagOpt = None, fromTagOpt = None,
@ -372,7 +372,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTestCachedBitcoindNewest {
for { for {
oldTransactions <- wallet.listTransactions() oldTransactions <- wallet.listTransactions()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
tx <- wallet.fundRawTransaction(Vector(dummyOutput), tx <- wallet.fundRawTransaction(Vector(dummyOutput),
feeRate, feeRate,
fromTagOpt = None, fromTagOpt = None,

View File

@ -279,7 +279,7 @@ class WalletIntegrationTest extends BitcoinSWalletTestCachedBitcoindNewest {
walletBal1 <- wallet.getBalance() walletBal1 <- wallet.getBalance()
// Create child tx // Create child tx
childFeeRate <- wallet.feeRateApi.getFeeRate childFeeRate <- wallet.feeRateApi.getFeeRate()
childTx <- wallet.bumpFeeCPFP(parentTx.txIdBE, childFeeRate) childTx <- wallet.bumpFeeCPFP(parentTx.txIdBE, childFeeRate)
_ <- bitcoind.sendRawTransaction(childTx) _ <- bitcoind.sendRawTransaction(childTx)

View File

@ -383,7 +383,7 @@ class WalletSendingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
for { for {
parent <- wallet.sendToAddress(testAddress, amountToSend, None) parent <- wallet.sendToAddress(testAddress, amountToSend, None)
bumpRate <- wallet.feeRateApi.getFeeRate bumpRate <- wallet.feeRateApi.getFeeRate()
child <- wallet.bumpFeeCPFP(parent.txIdBE, bumpRate) child <- wallet.bumpFeeCPFP(parent.txIdBE, bumpRate)
received <- wallet.spendingInfoDAO.findTx(child).map(_.nonEmpty) received <- wallet.spendingInfoDAO.findTx(child).map(_.nonEmpty)
@ -463,7 +463,7 @@ class WalletSendingTest extends BitcoinSWalletTest {
algo: CoinSelectionAlgo): Future[Assertion] = { algo: CoinSelectionAlgo): Future[Assertion] = {
for { for {
account <- wallet.getDefaultAccount() account <- wallet.getDefaultAccount()
feeRate <- wallet.getFeeRate feeRate <- wallet.getFeeRate()
allUtxos <- wallet.listUtxos(account.hdAccount) allUtxos <- wallet.listUtxos(account.hdAccount)
output = TransactionOutput(amountToSend, testAddress.scriptPubKey) output = TransactionOutput(amountToSend, testAddress.scriptPubKey)
expectedUtxos = expectedUtxos =