diff --git a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala index f504cea296..3aeca5694b 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala @@ -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 diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index 7fe4aee9b9..7ee8ccc087 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -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 diff --git a/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala index a82f287d0a..1372e10805 100644 --- a/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala +++ b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala @@ -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))) } diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index ce7ab3fa90..9f9c4baff5 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -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( diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index faeefc5cf5..c5dbdd00dc 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -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) + } } diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index a8a5492701..fb44318d4b 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -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)