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 fc49fea9c5..7c806c22c2 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -718,6 +718,56 @@ object ConsoleCli { case other => other })) ), + cmd("bumpfeecpfp") + .action((_, conf) => + conf.copy(command = BumpFeeCPFP(DoubleSha256DigestBE.empty, + SatoshisPerVirtualByte.zero))) + .text("Bump the fee of the given transaction id with a child tx using the given fee rate") + .children( + arg[DoubleSha256DigestBE]("txid") + .text("Id of transaction to bump fee") + .required() + .action((txid, conf) => + conf.copy(command = conf.command match { + case cpfp: BumpFeeCPFP => + cpfp.copy(txId = txid) + case other => other + })), + arg[SatoshisPerVirtualByte]("feerate") + .text("Fee rate in sats per virtual byte of the child transaction") + .required() + .action((feeRate, conf) => + conf.copy(command = conf.command match { + case cpfp: BumpFeeCPFP => + cpfp.copy(feeRate = feeRate) + case other => other + })) + ), + cmd("bumpfeerbf") + .action((_, conf) => + conf.copy(command = BumpFeeRBF(DoubleSha256DigestBE.empty, + SatoshisPerVirtualByte.zero))) + .text("Replace given transaction with one with the new fee rate") + .children( + arg[DoubleSha256DigestBE]("txid") + .text("Id of transaction to bump fee") + .required() + .action((txid, conf) => + conf.copy(command = conf.command match { + case rbf: BumpFeeRBF => + rbf.copy(txId = txid) + case other => other + })), + arg[SatoshisPerVirtualByte]("feerate") + .text("New fee rate in sats per virtual byte") + .required() + .action((feeRate, conf) => + conf.copy(command = conf.command match { + case rbf: BumpFeeRBF => + rbf.copy(feeRate = feeRate) + case other => other + })) + ), cmd("gettransaction") .action((_, conf) => conf.copy(command = GetTransaction(DoubleSha256DigestBE.empty))) @@ -1418,6 +1468,10 @@ object ConsoleCli { up.writeJs(bitcoins), up.writeJs(feeRateOpt), up.writeJs(algo))) + case BumpFeeCPFP(txId, feeRate) => + RequestParam("bumpfeecpfp", Seq(up.writeJs(txId), up.writeJs(feeRate))) + case BumpFeeRBF(txId, feeRate) => + RequestParam("bumpfeerbf", Seq(up.writeJs(txId), up.writeJs(feeRate))) case OpReturnCommit(message, hashMessage, satoshisPerVirtualByte) => RequestParam("opreturncommit", Seq(up.writeJs(message), @@ -1715,6 +1769,16 @@ object CliCommand { feeRateOpt: Option[SatoshisPerVirtualByte]) extends CliCommand + case class BumpFeeCPFP( + txId: DoubleSha256DigestBE, + feeRate: SatoshisPerVirtualByte) + extends CliCommand + + case class BumpFeeRBF( + txId: DoubleSha256DigestBE, + feeRate: SatoshisPerVirtualByte) + extends CliCommand + case class SignPSBT(psbt: PSBT) extends CliCommand case class LockUnspent( 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 283e32db98..8910a5fc4a 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 @@ -1032,6 +1032,50 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + "bump fee with rbf" in { + (mockWalletApi + .bumpFeeRBF(_: DoubleSha256DigestBE, _: FeeUnit)) + .expects(DoubleSha256DigestBE.empty, SatoshisPerVirtualByte.one) + .returning(Future.successful(EmptyTransaction)) + + (mockWalletApi.broadcastTransaction _) + .expects(EmptyTransaction) + .returning(FutureUtil.unit) + .anyNumberOfTimes() + + val route = walletRoutes.handleCommand( + ServerCommand("bumpfeerbf", + Arr(Str(DoubleSha256DigestBE.empty.hex), Num(1)))) + + Post() ~> route ~> check { + assert(contentType == `application/json`) + assert( + responseAs[String] == """{"result":"0000000000000000000000000000000000000000000000000000000000000000","error":null}""") + } + } + + "bump fee with CPFP" in { + (mockWalletApi + .bumpFeeCPFP(_: DoubleSha256DigestBE, _: FeeUnit)) + .expects(DoubleSha256DigestBE.empty, SatoshisPerVirtualByte.one) + .returning(Future.successful(EmptyTransaction)) + + (mockWalletApi.broadcastTransaction _) + .expects(EmptyTransaction) + .returning(FutureUtil.unit) + .anyNumberOfTimes() + + val route = walletRoutes.handleCommand( + ServerCommand("bumpfeecpfp", + Arr(Str(DoubleSha256DigestBE.empty.hex), Num(1)))) + + Post() ~> route ~> check { + assert(contentType == `application/json`) + assert( + responseAs[String] == """{"result":"0000000000000000000000000000000000000000000000000000000000000000","error":null}""") + } + } + "return the peer list" in { val route = nodeRoutes.handleCommand(ServerCommand("getpeers", Arr())) 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 93c85d616c..13ee281d67 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -689,6 +689,30 @@ object OpReturnCommit extends ServerJsonModels { } } +case class BumpFee(txId: DoubleSha256DigestBE, feeRate: SatoshisPerVirtualByte) + +object BumpFee extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[BumpFee] = { + jsArr.arr.toList match { + case txIdJs :: feeRateJs :: Nil => + Try { + val txId = DoubleSha256DigestBE(txIdJs.str) + val feeRate = SatoshisPerVirtualByte(Satoshis(feeRateJs.num.toLong)) + BumpFee(txId, feeRate) + } + case Nil => + Failure( + new IllegalArgumentException("Missing txId and fee rate arguments")) + + case other => + Failure( + new IllegalArgumentException( + s"Bad number of arguments: ${other.length}. Expected: 2")) + } + } +} + // Oracle Models case class CreateEvent( 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 eedc234571..83ef4b612c 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -321,6 +321,32 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit } } + case ServerCommand("bumpfeerbf", arr) => + BumpFee.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(BumpFee(txId, feeRate)) => + complete { + for { + tx <- wallet.bumpFeeRBF(txId, feeRate) + _ <- wallet.broadcastTransaction(tx) + } yield Server.httpSuccess(tx.txIdBE) + } + } + + case ServerCommand("bumpfeecpfp", arr) => + BumpFee.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(BumpFee(txId, feeRate)) => + complete { + for { + tx <- wallet.bumpFeeCPFP(txId, feeRate) + _ <- wallet.broadcastTransaction(tx) + } yield Server.httpSuccess(tx.txIdBE) + } + } + case ServerCommand("rescan", arr) => Rescan.fromJsArr(arr) match { case Failure(exception) => diff --git a/docs/applications/server.md b/docs/applications/server.md index 37c3e0572b..b5235e04a6 100644 --- a/docs/applications/server.md +++ b/docs/applications/server.md @@ -183,7 +183,13 @@ For more information on how to use our built in `cli` to interact with the serve - `opreturncommit` `message` `[options]` - Creates OP_RETURN commitment transaction - `message` - message to put into OP_RETURN commitment - `--hashMessage` - should the message be hashed before commitment - - `--feerate ` - Fee rate in sats per virtual byte + - `--feerate ` - Fee rate in sats per virtual byte + - `bumpfeecpfp` `txid` `feerate` - Bump the fee of the given transaction id with a child tx using the given fee rate + - `txid` - Id of transaction to bump fee + - `feerate` - Fee rate in sats per virtual byte of the child transaction + - `bumpfeerbf` `txid` `feerate` - Replace given transaction with one with the new fee rate + - `txid` - Id of transaction to bump fee + - `feerate` - New fee rate in sats per virtual byte - `gettransaction` `txid` - Get detailed information about in-wallet transaction - `txid` - The transaction id - `lockunspent` `unlock` `transactions` - Temporarily lock (unlock=false) or unlock (unlock=true) specified transaction outputs.