Add types for various bitcoind RPC exceptions

In protocol.h (Bitcoin Core) all RPC errors are enumerated. The
exceptions added here are copied directly from that file.
This commit is contained in:
Torkel Rogstad 2019-07-11 14:58:17 +02:00
parent b5aed58e26
commit 18e5d97720
9 changed files with 380 additions and 23 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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]
}

View file

@ -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

View file

@ -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

View file

@ -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/).