From da1aecaab5594fba36865798ec0a441b9d6dce44 Mon Sep 17 00:00:00 2001 From: Nadav Kohen Date: Tue, 4 Feb 2020 07:05:38 -0700 Subject: [PATCH] Console CLI (#1095) * Moved Cli code to an object called ConsoleCli which can be called from sbt console * Add --allow-incomplete-classpath to cli.sbt Co-authored-by: Ben Carman --- app/cli/cli.sbt | 4 +- .../src/main/scala/org/bitcoins/cli/Cli.scala | 385 +----------------- .../scala/org/bitcoins/cli/ConsoleCli.scala | 384 +++++++++++++++++ .../scala/org/bitcoins/server/Server.scala | 2 +- 4 files changed, 395 insertions(+), 380 deletions(-) create mode 100644 app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala diff --git a/app/cli/cli.sbt b/app/cli/cli.sbt index a7e873fba3..7f73cf18d7 100644 --- a/app/cli/cli.sbt +++ b/app/cli/cli.sbt @@ -4,7 +4,6 @@ libraryDependencies ++= Deps.cli(scalaVersion.value) graalVMNativeImageOptions ++= Seq( "-H:EnableURLProtocols=http", - "-H:+ReportExceptionStackTraces", // builds a stand-alone image or reports a failure "--no-fallback", @@ -12,7 +11,8 @@ graalVMNativeImageOptions ++= Seq( // I'm not sure why, though... "--initialize-at-build-time=scala.Function3", "--report-unsupported-elements-at-runtime", - "--verbose" + "--verbose", + "--allow-incomplete-classpath" ) enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin) diff --git a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala index e66987d3c7..4ee51db5fe 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala @@ -1,393 +1,24 @@ package org.bitcoins.cli import java.net.ConnectException -import java.{util => ju} -import org.bitcoins.cli.CliCommand._ -import org.bitcoins.cli.CliReaders._ -import org.bitcoins.core.config.NetworkParameters -import org.bitcoins.core.currency._ -import org.bitcoins.core.protocol._ -import org.bitcoins.core.protocol.transaction.{EmptyTransaction, Transaction} -import org.bitcoins.core.psbt.PSBT -import org.bitcoins.picklers._ -import scopt.OParser -import ujson.{Num, Str} -import upickle.{default => up} - -case class Config( - command: CliCommand = CliCommand.NoCommand, - network: Option[NetworkParameters] = None, - debug: Boolean = false -) - -sealed abstract class CliCommand - -object CliCommand { - case object NoCommand extends CliCommand - - // Wallet - case class SendToAddress(destination: BitcoinAddress, amount: Bitcoins) - extends CliCommand - case object GetNewAddress extends CliCommand - case object GetBalance extends CliCommand - - // Node - case object GetPeers extends CliCommand - - // Chain - case object GetBestBlockHash extends CliCommand - case object GetBlockCount extends CliCommand - case object GetFilterCount extends CliCommand - case object GetFilterHeaderCount extends CliCommand - case class Rescan( - addressBatchSize: Option[Int], - startBlock: Option[BlockStamp], - endBlock: Option[BlockStamp], - force: Boolean) - extends CliCommand - - // PSBT - case class CombinePSBTs(psbts: Seq[PSBT]) extends CliCommand - case class JoinPSBTs(psbts: Seq[PSBT]) extends CliCommand - case class FinalizePSBT(psbt: PSBT) extends CliCommand - case class ExtractFromPSBT(psbt: PSBT) extends CliCommand - case class ConvertToPSBT(transaction: Transaction) extends CliCommand -} +import scala.util.{Failure, Success} object Cli extends App { - val builder = OParser.builder[Config] - - val parser = { - import CliCommand._ - import builder._ - OParser.sequence( - programName("bitcoin-s-cli"), - opt[NetworkParameters]('n', "network") - .action((np, conf) => conf.copy(network = Some(np))) - .text("Select the active network."), - opt[Unit]("debug") - .action((_, conf) => conf.copy(debug = true)) - .text("Print debugging information"), - cmd("getblockcount") - .hidden() - .action((_, conf) => conf.copy(command = GetBlockCount)) - .text(s"Get the block height"), - cmd("getfiltercount") - .hidden() - .action((_, conf) => conf.copy(command = GetFilterCount)) - .text(s"Get the number of filters"), - cmd("getfilterheadercount") - .hidden() - .action((_, conf) => conf.copy(command = GetFilterHeaderCount)) - .text(s"Get the number of filter headers"), - cmd("getbestblockhash") - .hidden() - .action((_, conf) => conf.copy(command = GetBestBlockHash)) - .text(s"Get the best block hash"), - cmd("rescan") - .hidden() - .action( - (_, conf) => - conf.copy( - command = Rescan(addressBatchSize = Option.empty, - startBlock = Option.empty, - endBlock = Option.empty, - force = false))) - .text(s"Rescan UTXOs") - .children( - opt[Unit]("force") - .optional() - .action((_, conf) => - conf.copy(command = conf.command match { - case rescan: Rescan => - rescan.copy(force = true) - case other => other - })), - opt[Int]("batch-size") - .optional() - .action((batchSize, conf) => - conf.copy(command = conf.command match { - case rescan: Rescan => - rescan.copy(addressBatchSize = Option(batchSize)) - case other => other - })), - opt[BlockStamp]("start") - .optional() - .action((start, conf) => - conf.copy(command = conf.command match { - case rescan: Rescan => - rescan.copy(startBlock = Option(start)) - case other => other - })), - opt[BlockStamp]("end") - .optional() - .action((end, conf) => - conf.copy(command = conf.command match { - case rescan: Rescan => - rescan.copy(endBlock = Option(end)) - case other => other - })) - ), - cmd("getbalance") - .hidden() - .action((_, conf) => conf.copy(command = GetBalance)) - .text("Get the wallet balance"), - cmd("getnewaddress") - .hidden() - .action((_, conf) => conf.copy(command = GetNewAddress)) - .text("Get a new address"), - cmd("sendtoaddress") - .hidden() - .action( - // TODO how to handle null here? - (_, conf) => conf.copy(command = SendToAddress(null, 0.bitcoin))) - .text("Send money to the given address") - .children( - opt[BitcoinAddress]("address") - .required() - .action((addr, conf) => - conf.copy(command = conf.command match { - case send: SendToAddress => - send.copy(destination = addr) - case other => other - })), - opt[Bitcoins]("amount") - .required() - .action((btc, conf) => - conf.copy(command = conf.command match { - case send: SendToAddress => - send.copy(amount = btc) - case other => other - })) - ), - cmd("getpeers") - .hidden() - .action((_, conf) => conf.copy(command = GetPeers)) - .text(s"List the connected peers"), - cmd("combinepsbts") - .hidden() - .action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty))) - .text("Combines all the given PSBTs") - .children( - opt[Seq[PSBT]]("psbts") - .required() - .action((seq, conf) => - conf.copy(command = conf.command match { - case combinePSBTs: CombinePSBTs => - combinePSBTs.copy(psbts = seq) - case other => other - })) - ), - cmd("joinpsbts") - .hidden() - .action((_, conf) => conf.copy(command = JoinPSBTs(Seq.empty))) - .text("Combines all the given PSBTs") - .children( - opt[Seq[PSBT]]("psbts") - .required() - .action((seq, conf) => - conf.copy(command = conf.command match { - case joinPSBTs: JoinPSBTs => - joinPSBTs.copy(psbts = seq) - case other => other - })) - ), - cmd("finalizepsbt") - .hidden() - .action((_, conf) => conf.copy(command = FinalizePSBT(PSBT.empty))) - .text("Finalizes the given PSBT if it can") - .children( - opt[PSBT]("psbt") - .required() - .action((psbt, conf) => - conf.copy(command = conf.command match { - case finalizePSBT: FinalizePSBT => - finalizePSBT.copy(psbt = psbt) - case other => other - })) - ), - cmd("extractfrompsbt") - .hidden() - .action((_, conf) => conf.copy(command = ExtractFromPSBT(PSBT.empty))) - .text("Extracts a transaction from the given PSBT if it can") - .children( - opt[PSBT]("psbt") - .required() - .action((psbt, conf) => - conf.copy(command = conf.command match { - case extractFromPSBT: ExtractFromPSBT => - extractFromPSBT.copy(psbt = psbt) - case other => other - })) - ), - cmd("converttopsbt") - .hidden() - .action((_, conf) => - conf.copy(command = ConvertToPSBT(EmptyTransaction))) - .text("Creates an empty psbt from the given transaction") - .children( - opt[Transaction]("unsignedTx") - .required() - .action((tx, conf) => - conf.copy(command = conf.command match { - case convertToPSBT: ConvertToPSBT => - convertToPSBT.copy(transaction = tx) - case other => other - })) - ), - help('h', "help").text("Display this help message and exit"), - arg[String]("") - .optional() - .text( - "The command and arguments to be executed. Try bitcoin-s-cli help for a list of all commands"), - checkConfig { - case Config(NoCommand, _, _) => - failure("You need to provide a command!") - case _ => success - } - ) - } - - // TODO make this dynamic - val port = 9999 - val host = "localhost" - - val config: Config = OParser.parse(parser, args, Config()) match { - case None => sys.exit(1) - case Some(conf) => conf - } - import System.err.{println => printerr} - /** Prints the given message to stderr if debug is set */ - def debug(message: Any): Unit = { - if (config.debug) { - printerr(s"DEBUG: $message") - } - } - - /** Prints the given message to stderr and exist */ - def error(message: String): Nothing = { - printerr(message) - // TODO error codes? - sys.exit(1) - } - - case class RequestParam( - method: String, - params: Seq[ujson.Value.Value] = Nil) { - - lazy val toJsonMap: Map[String, ujson.Value] = { - Map("method" -> method, "params" -> params) - } - } - - val requestParam: RequestParam = config.command match { - case GetBalance => - RequestParam("getbalance") - case GetNewAddress => - RequestParam("getnewaddress") - case Rescan(addressBatchSize, startBlock, endBlock, force) => - RequestParam("rescan", - Seq(up.writeJs(addressBatchSize), - up.writeJs(startBlock), - up.writeJs(endBlock), - up.writeJs(force))) - - case SendToAddress(address, bitcoins) => - RequestParam("sendtoaddress", - Seq(up.writeJs(address), up.writeJs(bitcoins))) - // height - case GetBlockCount => RequestParam("getblockcount") - // filter count - case GetFilterCount => RequestParam("getfiltercount") - // filter header count - case GetFilterHeaderCount => RequestParam("getfilterheadercount") - // besthash - case GetBestBlockHash => RequestParam("getbestblockhash") - // peers - case GetPeers => RequestParam("getpeers") - // PSBTs - case CombinePSBTs(psbts) => - RequestParam("combinepsbts", Seq(up.writeJs(psbts))) - case JoinPSBTs(psbts) => - RequestParam("joinpsbts", Seq(up.writeJs(psbts))) - case FinalizePSBT(psbt) => - RequestParam("finalizepsbt", Seq(up.writeJs(psbt))) - case ExtractFromPSBT(psbt) => - RequestParam("extractfrompsbt", Seq(up.writeJs(psbt))) - case ConvertToPSBT(tx) => - RequestParam("converttopsbt", Seq(up.writeJs(tx))) - - case NoCommand => ??? - } - try { - - import com.softwaremill.sttp._ - implicit val backend = HttpURLConnectionBackend() - val request = - sttp - .post(uri"http://$host:$port/") - .contentType("application/json") - .body({ - val uuid = ju.UUID.randomUUID.toString - val paramsWithID: Map[String, ujson.Value] = requestParam.toJsonMap + ("id" -> up - .writeJs(uuid)) - up.write(paramsWithID) - }) - debug(s"HTTP request: $request") - val response = request.send() - - debug(s"HTTP response:") - debug(response) - - // in order to mimic Bitcoin Core we always send - // an object looking like {"result": ..., "error": ...} - val rawBody = response.body match { - case Left(err) => err - case Right(response) => response - } - - val js = ujson.read(rawBody) - val jsObj = try { - js.obj - } catch { - case _: Throwable => - error(s"Response was not a JSON object! Got: $rawBody") - } - - /** Gets the given key from jsObj if it exists - * and is not null */ - def getKey(key: String): Option[ujson.Value] = - jsObj - .get(key) - .flatMap(result => if (result.isNull) None else Some(result)) - - /** Converts a `ujson.Value` to String, making an - * effort to avoid preceding and trailing `"`s */ - def jsValueToString(value: ujson.Value) = value match { - case Str(string) => string - case Num(num) if num.isWhole => num.toLong.toString() - case Num(num) => num.toString() - case rest: ujson.Value => rest.toString() - } - - (getKey("result"), getKey("error")) match { - case (Some(result), None) => - val msg = jsValueToString(result) - println(msg) - case (None, Some(err)) => - val msg = jsValueToString(err) - error(msg) - case (None, None) | (Some(_), Some(_)) => - error(s"Got unexpected response: $rawBody") + ConsoleCli.exec(args.toVector: _*) match { + case Success(output) => println(output) + case Failure(err) => + printerr(err.getMessage) + sys.exit(1) } } catch { case _: ConnectException => - error( + printerr( "Connection refused! Check that the server is running and configured correctly.") + sys.exit(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 new file mode 100644 index 0000000000..94e2e3efac --- /dev/null +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -0,0 +1,384 @@ +package org.bitcoins.cli + +import org.bitcoins.cli.CliCommand._ +import org.bitcoins.cli.CliReaders._ +import org.bitcoins.core.config.NetworkParameters +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.picklers._ +import scopt.OParser +import ujson.{Num, Str} +import upickle.{default => up} + +import scala.collection.mutable +import scala.util.{Failure, Success, Try} + +object ConsoleCli { + + def parser: OParser[Unit, Config] = { + val builder = OParser.builder[Config] + + import builder._ + OParser.sequence( + programName("bitcoin-s-cli"), + opt[NetworkParameters]('n', "network") + .action((np, conf) => conf.copy(network = Some(np))) + .text("Select the active network."), + opt[Unit]("debug") + .action((_, conf) => conf.copy(debug = true)) + .text("Print debugging information"), + cmd("getblockcount") + .hidden() + .action((_, conf) => conf.copy(command = GetBlockCount)) + .text(s"Get the block height"), + cmd("getfiltercount") + .hidden() + .action((_, conf) => conf.copy(command = GetFilterCount)) + .text(s"Get the number of filters"), + cmd("getfilterheadercount") + .hidden() + .action((_, conf) => conf.copy(command = GetFilterHeaderCount)) + .text(s"Get the number of filter headers"), + cmd("getbestblockhash") + .hidden() + .action((_, conf) => conf.copy(command = GetBestBlockHash)) + .text(s"Get the best block hash"), + cmd("rescan") + .hidden() + .action( + (_, conf) => + conf.copy( + command = Rescan(addressBatchSize = Option.empty, + startBlock = Option.empty, + endBlock = Option.empty, + force = false))) + .text(s"Rescan UTXOs") + .children( + opt[Unit]("force") + .optional() + .action((_, conf) => + conf.copy(command = conf.command match { + case rescan: Rescan => + rescan.copy(force = true) + case other => other + })), + opt[Int]("batch-size") + .optional() + .action((batchSize, conf) => + conf.copy(command = conf.command match { + case rescan: Rescan => + rescan.copy(addressBatchSize = Option(batchSize)) + case other => other + })), + opt[BlockStamp]("start") + .optional() + .action((start, conf) => + conf.copy(command = conf.command match { + case rescan: Rescan => + rescan.copy(startBlock = Option(start)) + case other => other + })), + opt[BlockStamp]("end") + .optional() + .action((end, conf) => + conf.copy(command = conf.command match { + case rescan: Rescan => + rescan.copy(endBlock = Option(end)) + case other => other + })) + ), + cmd("getbalance") + .hidden() + .action((_, conf) => conf.copy(command = GetBalance)) + .text("Get the wallet balance"), + cmd("getnewaddress") + .hidden() + .action((_, conf) => conf.copy(command = GetNewAddress)) + .text("Get a new address"), + cmd("sendtoaddress") + .hidden() + .action( + // TODO how to handle null here? + (_, conf) => conf.copy(command = SendToAddress(null, 0.bitcoin))) + .text("Send money to the given address") + .children( + opt[BitcoinAddress]("address") + .required() + .action((addr, conf) => + conf.copy(command = conf.command match { + case send: SendToAddress => + send.copy(destination = addr) + case other => other + })), + opt[Bitcoins]("amount") + .required() + .action((btc, conf) => + conf.copy(command = conf.command match { + case send: SendToAddress => + send.copy(amount = btc) + case other => other + })) + ), + cmd("getpeers") + .hidden() + .action((_, conf) => conf.copy(command = GetPeers)) + .text(s"List the connected peers"), + cmd("combinepsbts") + .hidden() + .action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty))) + .text("Combines all the given PSBTs") + .children( + opt[Seq[PSBT]]("psbts") + .required() + .action((seq, conf) => + conf.copy(command = conf.command match { + case combinePSBTs: CombinePSBTs => + combinePSBTs.copy(psbts = seq) + case other => other + })) + ), + cmd("joinpsbts") + .hidden() + .action((_, conf) => conf.copy(command = JoinPSBTs(Seq.empty))) + .text("Combines all the given PSBTs") + .children( + opt[Seq[PSBT]]("psbts") + .required() + .action((seq, conf) => + conf.copy(command = conf.command match { + case joinPSBTs: JoinPSBTs => + joinPSBTs.copy(psbts = seq) + case other => other + })) + ), + cmd("finalizepsbt") + .hidden() + .action((_, conf) => conf.copy(command = FinalizePSBT(PSBT.empty))) + .text("Finalizes the given PSBT if it can") + .children( + opt[PSBT]("psbt") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case finalizePSBT: FinalizePSBT => + finalizePSBT.copy(psbt = psbt) + case other => other + })) + ), + cmd("extractfrompsbt") + .hidden() + .action((_, conf) => conf.copy(command = ExtractFromPSBT(PSBT.empty))) + .text("Extracts a transaction from the given PSBT if it can") + .children( + opt[PSBT]("psbt") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case extractFromPSBT: ExtractFromPSBT => + extractFromPSBT.copy(psbt = psbt) + case other => other + })) + ), + cmd("converttopsbt") + .hidden() + .action((_, conf) => + conf.copy(command = ConvertToPSBT(EmptyTransaction))) + .text("Creates an empty psbt from the given transaction") + .children( + opt[Transaction]("unsignedTx") + .required() + .action((tx, conf) => + conf.copy(command = conf.command match { + case convertToPSBT: ConvertToPSBT => + convertToPSBT.copy(transaction = tx) + case other => other + })) + ), + help('h', "help").text("Display this help message and exit"), + arg[String]("") + .optional() + .text( + "The command and arguments to be executed. Try bitcoin-s-cli help for a list of all commands"), + checkConfig { + case Config(NoCommand, _, _) => + failure("You need to provide a command!") + case _ => success + } + ) + } + + def exec(args: String*): Try[String] = { + val config = OParser.parse(parser, args.toVector, Config()) match { + case None => sys.exit(1) + case Some(conf) => conf + } + + import System.err.{println => printerr} + + /** Prints the given message to stderr if debug is set */ + def debug(message: Any): Unit = { + if (config.debug) { + printerr(s"DEBUG: $message") + } + } + + /** Prints the given message to stderr and exist */ + def error[T](message: String): Failure[T] = { + Failure(new RuntimeException(message)) + } + + val requestParam: RequestParam = config.command match { + case GetBalance => + RequestParam("getbalance") + case GetNewAddress => + RequestParam("getnewaddress") + case Rescan(addressBatchSize, startBlock, endBlock, force) => + RequestParam("rescan", + Seq(up.writeJs(addressBatchSize), + up.writeJs(startBlock), + up.writeJs(endBlock), + up.writeJs(force))) + + case SendToAddress(address, bitcoins) => + RequestParam("sendtoaddress", + Seq(up.writeJs(address), up.writeJs(bitcoins))) + // height + case GetBlockCount => RequestParam("getblockcount") + // filter count + case GetFilterCount => RequestParam("getfiltercount") + // filter header count + case GetFilterHeaderCount => RequestParam("getfilterheadercount") + // besthash + case GetBestBlockHash => RequestParam("getbestblockhash") + // peers + case GetPeers => RequestParam("getpeers") + // PSBTs + case CombinePSBTs(psbts) => + RequestParam("combinepsbts", Seq(up.writeJs(psbts))) + case JoinPSBTs(psbts) => + RequestParam("joinpsbts", Seq(up.writeJs(psbts))) + case FinalizePSBT(psbt) => + RequestParam("finalizepsbt", Seq(up.writeJs(psbt))) + case ExtractFromPSBT(psbt) => + RequestParam("extractfrompsbt", Seq(up.writeJs(psbt))) + case ConvertToPSBT(tx) => + RequestParam("converttopsbt", Seq(up.writeJs(tx))) + + case NoCommand => ??? + } + + Try { + import com.softwaremill.sttp._ + implicit val backend = HttpURLConnectionBackend() + val request = + sttp + .post(uri"http://$host:$port/") + .contentType("application/json") + .body({ + val uuid = java.util.UUID.randomUUID.toString + val paramsWithID: Map[String, ujson.Value] = requestParam.toJsonMap + ("id" -> up + .writeJs(uuid)) + up.write(paramsWithID) + }) + debug(s"HTTP request: $request") + val response = request.send() + + debug(s"HTTP response:") + debug(response) + + // in order to mimic Bitcoin Core we always send + // an object looking like {"result": ..., "error": ...} + val rawBody = response.body match { + case Left(err) => err + case Right(response) => response + } + + val js = ujson.read(rawBody) + val jsObjT = + Try(js.obj).transform[mutable.LinkedHashMap[String, ujson.Value]]( + Success(_), + _ => error(s"Response was not a JSON object! Got: $rawBody")) + + /** Gets the given key from jsObj if it exists + * and is not null */ + def getKey(key: String): Option[ujson.Value] = { + jsObjT.toOption.flatMap(_.get(key).flatMap(result => + if (result.isNull) None else Some(result))) + } + + /** Converts a `ujson.Value` to String, making an + * effort to avoid preceding and trailing `"`s */ + def jsValueToString(value: ujson.Value) = value match { + case Str(string) => string + case Num(num) if num.isWhole => num.toLong.toString + case Num(num) => num.toString + case rest: ujson.Value => rest.toString() + } + + (getKey("result"), getKey("error")) match { + case (Some(result), None) => + Success(jsValueToString(result)) + case (None, Some(err)) => + val msg = jsValueToString(err) + error(msg) + case (None, None) | (Some(_), Some(_)) => + error(s"Got unexpected response: $rawBody") + } + }.flatten + } + + // TODO make this dynamic + def port = 9999 + def host = "localhost" + + case class RequestParam( + method: String, + params: Seq[ujson.Value.Value] = Nil) { + + lazy val toJsonMap: Map[String, ujson.Value] = { + Map("method" -> method, "params" -> params) + } + } +} + +case class Config( + command: CliCommand = CliCommand.NoCommand, + network: Option[NetworkParameters] = None, + debug: Boolean = false +) + +sealed abstract class CliCommand + +object CliCommand { + case object NoCommand extends CliCommand + + // Wallet + case class SendToAddress(destination: BitcoinAddress, amount: Bitcoins) + extends CliCommand + case object GetNewAddress extends CliCommand + case object GetBalance extends CliCommand + + // Node + case object GetPeers extends CliCommand + + // Chain + case object GetBestBlockHash extends CliCommand + case object GetBlockCount extends CliCommand + case object GetFilterCount extends CliCommand + case object GetFilterHeaderCount extends CliCommand + case class Rescan( + addressBatchSize: Option[Int], + startBlock: Option[BlockStamp], + endBlock: Option[BlockStamp], + force: Boolean) + extends CliCommand + + // PSBT + case class CombinePSBTs(psbts: Seq[PSBT]) extends CliCommand + case class JoinPSBTs(psbts: Seq[PSBT]) extends CliCommand + case class FinalizePSBT(psbt: PSBT) extends CliCommand + case class ExtractFromPSBT(psbt: PSBT) extends CliCommand + case class ConvertToPSBT(transaction: Transaction) extends CliCommand +} diff --git a/app/server/src/main/scala/org/bitcoins/server/Server.scala b/app/server/src/main/scala/org/bitcoins/server/Server.scala index 5151f7cb55..46590cc724 100644 --- a/app/server/src/main/scala/org/bitcoins/server/Server.scala +++ b/app/server/src/main/scala/org/bitcoins/server/Server.scala @@ -48,7 +48,7 @@ case class Server(conf: AppConfig, handlers: Seq[ServerRoute])( StatusCodes.BadRequest)) case err: Throwable => logger.info(s"Unhandled error in server:", err) - complete(Server.httpError("There was an error")) + complete(Server.httpError(s"Request failed: ${err.getMessage}")) } handleRejections(rejectionHandler) {