mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-23 22:56:52 +01:00
Custom fee rate for wallet sends
This commit is contained in:
parent
2f3c5c5541
commit
5e4d23d562
6 changed files with 67 additions and 26 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue