Custom fee rate for wallet sends

This commit is contained in:
Ben Carman 2020-03-25 11:23:12 -05:00
parent 2f3c5c5541
commit 5e4d23d562
6 changed files with 67 additions and 26 deletions

View file

@ -8,6 +8,7 @@ import org.bitcoins.core.protocol.BlockStamp.BlockTime
import org.bitcoins.core.protocol._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import scopt._
/** scopt readers for parsing CLI params and options */
@ -44,6 +45,14 @@ object CliReaders {
val reads: String => Bitcoins = str => Bitcoins(BigDecimal(str))
}
implicit val satoshisPerVirtualByteReads: Read[SatoshisPerVirtualByte] =
new Read[SatoshisPerVirtualByte] {
val arity: Int = 1
val reads: String => SatoshisPerVirtualByte = str =>
SatoshisPerVirtualByte(Satoshis(BigInt(str)))
}
implicit val blockStampReads: Read[BlockStamp] =
new Read[BlockStamp] {
val arity: Int = 1

View file

@ -7,6 +7,7 @@ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction.{EmptyTransaction, Transaction}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.picklers._
import scopt.OParser
import ujson.{Num, Str}
@ -111,11 +112,11 @@ object ConsoleCli {
cmd("sendtoaddress")
.action(
// TODO how to handle null here?
(_, conf) => conf.copy(command = SendToAddress(null, 0.bitcoin)))
(_, conf) => conf.copy(command = SendToAddress(null, 0.bitcoin, None)))
.text("Send money to the given address")
.children(
arg[BitcoinAddress]("address")
.text("Adress to send to")
.text("Address to send to")
.required()
.action((addr, conf) =>
conf.copy(command = conf.command match {
@ -131,6 +132,15 @@ object ConsoleCli {
case send: SendToAddress =>
send.copy(amount = btc)
case other => other
})),
opt[SatoshisPerVirtualByte]("feerate")
.text("Fee rate in sats per virtual byte")
.optional()
.action((feeRate, conf) =>
conf.copy(command = conf.command match {
case send: SendToAddress =>
send.copy(satoshisPerVirtualByte = Some(feeRate))
case other => other
}))
),
note(sys.props("line.separator") + "=== Network ==="),
@ -249,9 +259,11 @@ object ConsoleCli {
up.writeJs(endBlock),
up.writeJs(force)))
case SendToAddress(address, bitcoins) =>
case SendToAddress(address, bitcoins, satoshisPerVirtualByte) =>
RequestParam("sendtoaddress",
Seq(up.writeJs(address), up.writeJs(bitcoins)))
Seq(up.writeJs(address),
up.writeJs(bitcoins),
up.writeJs(satoshisPerVirtualByte)))
// height
case GetBlockCount => RequestParam("getblockcount")
// filter count
@ -363,7 +375,10 @@ object CliCommand {
case object NoCommand extends CliCommand
// Wallet
case class SendToAddress(destination: BitcoinAddress, amount: Bitcoins)
case class SendToAddress(
destination: BitcoinAddress,
amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte])
extends CliCommand
case object GetNewAddress extends CliCommand
case class GetBalance(isSats: Boolean) extends CliCommand

View file

@ -4,6 +4,7 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import upickle.default._
package object picklers {
@ -26,4 +27,9 @@ package object picklers {
implicit val transactionPickler: ReadWriter[Transaction] =
readwriter[String].bimap(_.hex, Transaction.fromHex)
implicit val satoshisPerVirtualBytePickler: ReadWriter[
SatoshisPerVirtualByte] =
readwriter[Long].bimap(_.currencyUnit.satoshis.toLong,
l => SatoshisPerVirtualByte(Satoshis(l)))
}

View file

@ -249,7 +249,8 @@ class RoutesSpec
.anyNumberOfTimes()
val route = walletRoutes.handleCommand(
ServerCommand("sendtoaddress", Arr(Str(testAddressStr), Num(100))))
ServerCommand("sendtoaddress",
Arr(Str(testAddressStr), Num(100), Num(4))))
Post() ~> route ~> check {
contentType shouldEqual `application/json`
@ -259,7 +260,7 @@ class RoutesSpec
// negative cases
val route1 = walletRoutes.handleCommand(
ServerCommand("sendtoaddress", Arr(Null, Null)))
ServerCommand("sendtoaddress", Arr(Null, Null, Null)))
Post() ~> route1 ~> check {
rejection shouldEqual ValidationRejection(
@ -268,7 +269,7 @@ class RoutesSpec
}
val route2 = walletRoutes.handleCommand(
ServerCommand("sendtoaddress", Arr("Null", Null)))
ServerCommand("sendtoaddress", Arr("Null", Null, Null)))
Post() ~> route2 ~> check {
rejection shouldEqual ValidationRejection(
@ -277,7 +278,7 @@ class RoutesSpec
}
val route3 = walletRoutes.handleCommand(
ServerCommand("sendtoaddress", Arr(Str(testAddressStr), Null)))
ServerCommand("sendtoaddress", Arr(Str(testAddressStr), Null, Null)))
Post() ~> route3 ~> check {
rejection shouldEqual ValidationRejection(
@ -286,7 +287,8 @@ class RoutesSpec
}
val route4 = walletRoutes.handleCommand(
ServerCommand("sendtoaddress", Arr(Str(testAddressStr), Str("abc"))))
ServerCommand("sendtoaddress",
Arr(Str(testAddressStr), Str("abc"), Null)))
Post() ~> route4 ~> check {
rejection shouldEqual ValidationRejection(

View file

@ -1,10 +1,11 @@
package org.bitcoins.server
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import ujson._
import upickle.default._
@ -98,13 +99,6 @@ object Rescan extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[Rescan] = {
def nullToOpt(value: Value): Option[Value] = value match {
case Null => None
case Arr(arr) if arr.isEmpty => None
case Arr(arr) if arr.size == 1 => Some(arr.head)
case _: Value => Some(value)
}
def parseBlockStamp(value: Value): Option[BlockStamp] =
nullToOpt(value).map {
case Str(value) => BlockStamp.fromString(value).get
@ -154,7 +148,10 @@ object Rescan extends ServerJsonModels {
}
case class SendToAddress(address: BitcoinAddress, amount: Bitcoins)
case class SendToAddress(
address: BitcoinAddress,
amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte])
object SendToAddress extends ServerJsonModels {
@ -162,20 +159,24 @@ object SendToAddress extends ServerJsonModels {
// custom akka-http directive?
def fromJsArr(jsArr: ujson.Arr): Try[SendToAddress] = {
jsArr.arr.toList match {
case addrJs :: bitcoinsJs :: Nil =>
case addrJs :: bitcoinsJs :: satsPerVBytesJs :: Nil =>
Try {
val address = jsToBitcoinAddress(addrJs)
val bitcoins = Bitcoins(bitcoinsJs.num)
SendToAddress(address, bitcoins)
val satoshisPerVirtualByte =
nullToOpt(satsPerVBytesJs).map(satsPerVBytes =>
SatoshisPerVirtualByte(Satoshis(satsPerVBytes.num.toLong)))
SendToAddress(address, bitcoins, satoshisPerVirtualByte)
}
case Nil =>
Failure(
new IllegalArgumentException("Missing address and amount argument"))
new IllegalArgumentException(
"Missing address, amount, and fee rate arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 2"))
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
@ -200,4 +201,10 @@ trait ServerJsonModels {
def jsToTx(js: Value): Transaction = Transaction.fromHex(js.str)
def nullToOpt(value: Value): Option[Value] = value match {
case Null => None
case Arr(arr) if arr.isEmpty => None
case Arr(arr) if arr.size == 1 => Some(arr.head)
case _: Value => Some(value)
}
}

View file

@ -50,10 +50,12 @@ case class WalletRoutes(wallet: UnlockedWalletApi, node: Node)(
SendToAddress.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(SendToAddress(address, bitcoins)) =>
case Success(
SendToAddress(address, bitcoins, satoshisPerVirtualByteOpt)) =>
complete {
// TODO dynamic fees
val feeRate = SatoshisPerByte(100.sats)
// TODO dynamic fees based off mempool and recent blocks
val feeRate =
satoshisPerVirtualByteOpt.getOrElse(SatoshisPerByte(100.satoshis))
wallet.sendToAddress(address, bitcoins, feeRate).map { tx =>
node.broadcastTransaction(tx)
Server.httpSuccess(tx.txIdBE)