diff --git a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/MiningRpcTest.scala b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/MiningRpcTest.scala index 18e5b2c0a2..986eb37000 100644 --- a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/MiningRpcTest.scala +++ b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/MiningRpcTest.scala @@ -5,6 +5,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.util.BitcoindRpcTest import scala.concurrent.Future +import org.bitcoins.rpc.BitcoindP2PException.NotConnected class MiningRpcTest extends BitcoindRpcTest { lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] = @@ -20,10 +21,7 @@ class MiningRpcTest extends BitcoindRpcTest { .recover { // getblocktemplate is having a bad time on regtest // https://github.com/bitcoin/bitcoin/issues/11379 - case err: Throwable - if err.getMessage - .contains("-9") => - succeed + case NotConnected(_) => succeed case other: Throwable => throw other } .map(_ => succeed) diff --git a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/NodeRpcTest.scala b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/NodeRpcTest.scala index 9f593e08e0..0bb1e97ba6 100644 --- a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/NodeRpcTest.scala +++ b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/NodeRpcTest.scala @@ -6,6 +6,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.util.BitcoindRpcTest import scala.concurrent.Future +import org.bitcoins.rpc.BitcoindException.MiscError class NodeRpcTest extends BitcoindRpcTest { lazy val clientF: Future[BitcoindRpcClient] = @@ -18,7 +19,7 @@ class NodeRpcTest extends BitcoindRpcTest { // generate some extra blocks so rescan isn't too quick client.generate(3000).flatMap { _ => val rescanFailedF = - recoverToSucceededIf[RuntimeException](client.rescanBlockChain()) + recoverToSucceededIf[MiscError](client.rescanBlockChain()) client.abortRescan().flatMap { _ => rescanFailedF } diff --git a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/RawTransactionRpcTest.scala b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/RawTransactionRpcTest.scala index 3329f940fc..212d997703 100644 --- a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/RawTransactionRpcTest.scala +++ b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/RawTransactionRpcTest.scala @@ -16,6 +16,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.util.BitcoindRpcTest import scala.concurrent.Future +import org.bitcoins.rpc.BitcoindException.InvalidAddressOrKey class RawTransactionRpcTest extends BitcoindRpcTest { lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] = @@ -249,7 +250,7 @@ class RawTransactionRpcTest extends BitcoindRpcTest { client .createRawTransaction(Vector(), Map(address -> Bitcoins(1))) .flatMap { tx => - recoverToSucceededIf[RuntimeException]( + recoverToSucceededIf[InvalidAddressOrKey]( client.abandonTransaction(tx.txId)) } } diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/BitcoindException.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/BitcoindException.scala new file mode 100644 index 0000000000..c179389f5e --- /dev/null +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/BitcoindException.scala @@ -0,0 +1,313 @@ +package org.bitcoins.rpc +import play.api.libs.json.Reads +import play.api.libs.json.{JsResult, JsValue} +import play.api.libs.json.JsError +import play.api.libs.json.JsSuccess + +/** + * Represents failures that can happen when using the + * `bitcoind` RPC interface. + * + * @see [[https://github.com/bitcoin/bitcoin/blob/eb7daf4d600eeb631427c018a984a77a34aca66e/src/rpc/protocol.h#L32 protcol.h]] + * for an enumeration of all error codes used + */ +sealed abstract class BitcoindException(private val message: String) + extends Exception { + override def getMessage(): String = s"Error $code: $message" + val code: Int +} + +/** + * Wallet errors from `bitcoind` RPC calls + * + * @see [[https://github.com/bitcoin/bitcoin/blob/eb7daf4d600eeb631427c018a984a77a34aca66e/src/rpc/protocol.h#L32 protcol.h]] + * for an enumeration of all error codes used + */ + +object BitcoindException { + import org.bitcoins.rpc.BitcoindP2PException._ + import org.bitcoins.rpc.BitcoindWalletException._ + + implicit val reads: Reads[BitcoindException] = new Reads[BitcoindException] { + + def reads(json: JsValue): JsResult[BitcoindException] = + for { + code <- (json \ "code").validate[Int] + message <- (json \ "message").validate[String] + exception <- BitcoindException.fromCodeAndMessage(code, message) match { + case None => + JsError( + s"Could not construct bitcoind exception with code $code and message '$message'") + case Some(value) => JsSuccess(value) + } + } yield exception + } + + private val all: List[String => BitcoindException] = List( + InvalidParams(_), + InternalError(_), + ParseError(_), + MiscError(_), + TypeError(_), + InvalidAddressOrKey(_), + OutOfMemory(_), + InvalidParameter(_), + DatabaseError(_), + DeserializationError(_), + VerifyError(_), + VerifyRejected(_), + VerifyAlreadyInChain(_), + InWarmUp(_), + MethodDeprecated(_), + ForbiddenBySafeMode(_), + InInitialDownload(_), + NodeAlreadyAdded(_), + NodeNotAdded(_), + NodeNotConnected(_), + InvalidIpOrSubnet(_), + P2PDisabled(_), + WalletError(_), + InsufficientFunds(_), + InvalidLabelName(_), + KeypoolRanOut(_), + UnlockNeeded(_), + PassphraseIncorrect(_), + WrongEncState(_), + EncryptionFailed(_), + AlreadyUnlocked(_), + NotFound(_), + NotSpecified(_) + ) + + /** Attempts to construct a BitcoindException from the given code and message */ + def fromCodeAndMessage( + code: Int, + message: String): Option[BitcoindException] = { + + val constructorOpt = all.find(func => func(message).code == code) + + constructorOpt.map(func => func(message)) + } + + final case class InvalidParams(private val message: String) + extends BitcoindException(message) { + val code: Int = -32602 + } + + /** + * InternalError is only used for genuine errors in bitcoind + * (for example datadir corruption) + */ + final case class InternalError(private val message: String) + extends BitcoindException(message) { + val code: Int = -32603 + } + final case class ParseError(private val message: String) + extends BitcoindException(message) { + val code: Int = -32700 + } + + /** `std::exception` thrown in command handling*/ + final case class MiscError(private val message: String) + extends BitcoindException(message) { + val code: Int = -1 + } + + /** Unexpected type was passed as parameter */ + final case class TypeError(private val message: String) + extends BitcoindException(message) { + val code: Int = -3 + } + + /** Invalid address or key */ + final case class InvalidAddressOrKey(private val message: String) + extends BitcoindException(message) { + val code: Int = -5 + } + + /** Ran out of memory during operation*/ + final case class OutOfMemory(private val message: String) + extends BitcoindException(message) { + val code: Int = -7 + } + + /** Invalid, missing or duplicate parameter */ + final case class InvalidParameter(private val message: String) + extends BitcoindException(message) { + val code: Int = -8 + } + + /** Database error */ + final case class DatabaseError(private val message: String) + extends BitcoindException(message) { + val code: Int = -20 + } + + /** Error parsing or validating structure in raw format */ + final case class DeserializationError(private val message: String) + extends BitcoindException(message) { + val code: Int = -22 + } + + /** General error during transaction or block submission */ + final case class VerifyError(private val message: String) + extends BitcoindException(message) { + val code: Int = -25 + } + + /** Transaction or block was rejected by network rules */ + final case class VerifyRejected(private val message: String) + extends BitcoindException(message) { + val code: Int = -26 + } + + /** Transaction already in chain */ + final case class VerifyAlreadyInChain(private val message: String) + extends BitcoindException(message) { + val code: Int = -27 + } + + /** Client still warming up */ + final case class InWarmUp(private val message: String) + extends BitcoindException(message) { + val code: Int = -28 + } + + /** RPC method is deprecated */ + final case class MethodDeprecated(private val message: String) + extends BitcoindException(message) { + val code: Int = -32 + } + + /** Server is in safe mode, and command is not allowed in safe mode */ + final case class ForbiddenBySafeMode(private val message: String) + extends BitcoindException(message) { + val code: Int = -2 + } +} + +/** P2P client errors + * + * @see [[https://github.com/bitcoin/bitcoin/blob/eb7daf4d600eeb631427c018a984a77a34aca66e/src/rpc/protocol.h#L32 protcol.h]] + * for an enumeration of all error codes used + */ +sealed abstract class BitcoindP2PException(private val message: String) + extends BitcoindException(message) + +object BitcoindP2PException { + + /** Bitcoin is not connected */ + final case class NotConnected(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -9 + } + + /** Still downloading initial blocks */ + final case class InInitialDownload(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -10 + } + + /** Node is already added */ + final case class NodeAlreadyAdded(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -23 + } + + /** Node has not been added before */ + final case class NodeNotAdded(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -24 + } + + /** Node to disconnect not found in connected nodes */ + final case class NodeNotConnected(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -29 + } + + /** Invalid IP/Subnet */ + final case class InvalidIpOrSubnet(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -30 + } + + /** No valid connection manager instance found */ + final case class P2PDisabled(private val message: String) + extends BitcoindP2PException(message) { + val code: Int = -31 + } +} + +sealed abstract class BitcoindWalletException(private val message: String) + extends BitcoindException(message) + +object BitcoindWalletException { + + /** Unspecified problem with wallet (key not found etc.) */ + final case class WalletError(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -4 + } + + /** Not enough funds in wallet or account */ + final case class InsufficientFunds(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -6 + } + + /** Invalid label name */ + final case class InvalidLabelName(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -11 + } + + /** Keypool ran out, call keypoolrefill first */ + final case class KeypoolRanOut(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -12 + } + + /** Enter the wallet passphrase with walletpassphrase first */ + final case class UnlockNeeded(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -13 + } + + /** The wallet passphrase entered was incorrect */ + final case class PassphraseIncorrect(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -14 + } + + /** Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.) */ + final case class WrongEncState(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -15 + } + + /** Failed to encrypt the wallet */ + final case class EncryptionFailed(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -16 + } + + /** Wallet is already unlocked */ + final case class AlreadyUnlocked(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -17 + } + + /** Invalid wallet specified */ + final case class NotFound(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -18 + } + + /** No wallet specified (error when there are multiple wallets loaded) */ + final case class NotSpecified(private val message: String) + extends BitcoindWalletException(message) { + val code: Int = -19 + } + +} diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/BitcoindRpcClient.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/BitcoindRpcClient.scala index 370bd9510e..b591fe4f74 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/BitcoindRpcClient.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/BitcoindRpcClient.scala @@ -13,6 +13,11 @@ import org.bitcoins.rpc.config.{BitcoindConfig, BitcoindInstance} * [[org.bitcoins.rpc.client.v16.BitcoindV16RpcClient BitcoindV16RpcClient]] * or * [[org.bitcoins.rpc.client.v17.BitcoindV17RpcClient BitcoindV17RpcClient]]. + * + * If a RPC call fails for any reason, a + * [[org.bitcoins.rpc.BitcoindException BitcoindException]] is thrown. + * This is a sealed abstract class, so you can pattern match easily + * on the errors, and handle them as you see fit. */ class BitcoindRpcClient(val instance: BitcoindInstance)( implicit diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/Client.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/Client.scala index e57e13b43e..d91e7e6693 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/Client.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/common/Client.scala @@ -26,6 +26,7 @@ import org.bitcoins.rpc.config.BitcoindAuthCredentials.PasswordBased import java.nio.file.Path import org.bitcoins.rpc.config.BitcoindAuthCredentials import com.fasterxml.jackson.core.JsonParseException +import org.bitcoins.rpc.BitcoindException /** * This is the base trait for Bitcoin Core @@ -183,9 +184,9 @@ trait Client extends BitcoinSLogger { // Ping successful if no error can be parsed from the payload val parsedF = payloadF.map { payload => - (payload \ errorKey).validate[RpcError] match { - case _: JsSuccess[RpcError] => false - case _: JsError => true + (payload \ errorKey).validate[BitcoindException] match { + case _: JsSuccess[BitcoindException] => false + case _: JsError => true } } @@ -195,6 +196,7 @@ trait Client extends BitcoinSLogger { false } } + instance.authCredentials match { case cookie: CookieBased if Files.notExists(cookie.cookiePath) => // if the cookie file doesn't exist we're not started @@ -308,13 +310,12 @@ trait Client extends BitcoinSLogger { result match { case JsSuccess(value, _) => value case res: JsError => - (json \ errorKey).validate[RpcError] match { - case err: JsSuccess[RpcError] => + (json \ errorKey).validate[BitcoindException] match { + case JsSuccess(err, _) => if (printError) { - logger.error(s"Error ${err.value.code}: ${err.value.message}") + logger.error(s"$err") } - throw new RuntimeException( - s"Error $command ${err.value.code}: ${err.value.message}") + throw err case _: JsError => val jsonResult = (json \ resultKey).get val errString = @@ -332,20 +333,15 @@ trait Client extends BitcoinSLogger { json: JsValue, printError: Boolean): Unit = { if (result == JsSuccess(())) { - (json \ errorKey).validate[RpcError] match { - case err: JsSuccess[RpcError] => + (json \ errorKey).validate[BitcoindException] match { + case JsSuccess(err, _) => if (printError) { - logger.error(s"Error ${err.value.code}: ${err.value.message}") + logger.error(s"$err") } - throw new RuntimeException( - s"Error ${err.value.code}: ${err.value.message}") + throw err case _: JsError => } } } - case class RpcError(code: Int, message: String) - - implicit val rpcErrorReads: Reads[RpcError] = Json.reads[RpcError] - } diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v16/BitcoindV16RpcClient.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v16/BitcoindV16RpcClient.scala index 7ee6cebb1c..2bb2145a92 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v16/BitcoindV16RpcClient.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v16/BitcoindV16RpcClient.scala @@ -20,6 +20,8 @@ import scala.util.Try /** * This class is compatible with version 0.16 of Bitcoin Core. + * + * @see [[org.bitcoins.rpc.client.common.BitcoindRpcClient BitcoindRpcClient Scaladocs]] */ class BitcoindV16RpcClient(override val instance: BitcoindInstance)( implicit diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v17/BitcoindV17RpcClient.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v17/BitcoindV17RpcClient.scala index 0d7fd5a991..3bb1a88b2f 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v17/BitcoindV17RpcClient.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/client/v17/BitcoindV17RpcClient.scala @@ -26,6 +26,8 @@ import scala.util.Try /** * This class is compatible with version 0.17 of Bitcoin Core. * + * @see [[org.bitcoins.rpc.client.common.BitcoindRpcClient BitcoindRpcClient Scaladocs]] + * * @define signRawTx Bitcoin Core 0.17 had a breaking change in the API * for signing raw transactions. Previously the same * RPC call was used for signing a TX with existing keys diff --git a/docs/rpc/bitcoind.md b/docs/rpc/bitcoind.md index c3bfcb5e3b..ee1b5794b3 100644 --- a/docs/rpc/bitcoind.md +++ b/docs/rpc/bitcoind.md @@ -87,6 +87,45 @@ rpcCli.getBalance.onComplete { case balance => } ``` +## Error handling + +All errors returned by Bitcoin Core are mapped to a corresponding +[`BitcoindException`](https://github.com/bitcoin-s/bitcoin-s/blob/master/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/BitcoindException.scala). +These exceptions contain an error code and a message. `BitcoindException` is a sealed +trait, which means you can easily pattern match exhaustively. Of course, other errors +could also happen: network errors, stack overflows or out-of-memory errors. The provided +class is only intended to cover errors returned by Bitcoin Core. An example of how error +handling could look: + +```scala mdoc:compile-only +import org.bitcoins.rpc.client.common._ +import org.bitcoins.rpc.BitcoindException +import org.bitcoins.rpc.BitcoindWalletException +import org.bitcoins.core.crypto._ +import org.bitcoins.core.protocol._ +import org.bitcoins.core.currency._ + +import scala.concurrent._ + +implicit val system = akka.actor.ActorSystem() +implicit val ec = system.dispatcher + +// let's assume you have an already running client, +// so there's no need to start this one +val cli = BitcoindRpcClient.fromDatadir() + +// let's also assume you have a bitcoin address +val address: BitcoinAddress = ??? + +val txid: Future[DoubleSha256DigestBE] = + cli.sendToAddress(address, 3.bitcoins).recoverWith { + case BitcoindWalletException.UnlockNeeded(_) => + cli.walletPassphrase("my_passphrase", 60).flatMap { _ => + cli.sendToAddress(address, 3.bitcoins) + } + } +``` + ## Testing To test the Bitcoin-S RPC project you need both version 0.16 and 0.17 of Bitcoin Core. A list of current and previous releases can be found [here](https://bitcoincore.org/en/releases/).