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 205bdc066f..64f1c4e7b3 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -72,6 +72,19 @@ object ConsoleCli { cmd("getbestblockhash") .action((_, conf) => conf.copy(command = GetBestBlockHash)) .text(s"Get the best block hash"), + cmd("getblockheader") + .action((_, conf) => + conf.copy(command = GetBlockHeader(DoubleSha256DigestBE.empty))) + .text("Returns information about block header ") + .children( + arg[DoubleSha256DigestBE]("hash") + .text("The block hash") + .required() + .action((hash, conf) => + conf.copy(command = conf.command match { + case gbh: GetBlockHeader => gbh.copy(hash = hash) + case other => other + }))), cmd("decoderawtransaction") .action((_, conf) => conf.copy(command = DecodeRawTransaction(EmptyTransaction))) @@ -1499,6 +1512,8 @@ object ConsoleCli { up.writeJs(xprv), up.writeJs(passwordOpt))) + case GetBlockHeader(hash) => + RequestParam("getblockheader", Seq(up.writeJs(hash))) // height case GetBlockCount => RequestParam("getblockcount") // filter count @@ -1842,6 +1857,7 @@ object CliCommand { case object GetBlockCount extends CliCommand case object GetFilterCount extends CliCommand case object GetFilterHeaderCount extends CliCommand + case class GetBlockHeader(hash: DoubleSha256DigestBE) extends CliCommand case class DecodeRawTransaction(transaction: Transaction) extends CliCommand case class Rescan( 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 8910a5fc4a..83bdbc8825 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 @@ -1,12 +1,12 @@ package org.bitcoins.server import java.time.{ZoneId, ZonedDateTime} - import akka.http.scaladsl.model.ContentTypes._ import akka.http.scaladsl.server.ValidationRejection import akka.http.scaladsl.testkit.ScalatestRouteTest import org.bitcoins.core.Core import org.bitcoins.core.api.chain.ChainApi +import org.bitcoins.core.api.chain.db._ import org.bitcoins.core.api.wallet.db._ import org.bitcoins.core.api.wallet.{AddressInfo, CoinSelectionAlgo} import org.bitcoins.core.config.RegTest @@ -20,6 +20,7 @@ import org.bitcoins.core.protocol.BlockStamp.{ BlockTime, InvalidBlockStamp } +import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.script.EmptyScriptWitness import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp, P2PKHAddress} @@ -217,6 +218,44 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + val blockHeader = BlockHeader( + "00e0002094c6692e100ed20d14f8c325c897259749e781d55ed1b7eb1000000000000000309a90b49f5f5a14ffdb2857557f6f27a136943603fb29e65e283dcb27fd886124fee25f57e53019886c0e8b") + val blockHeaderDb = + BlockHeaderDbHelper.fromBlockHeader(height = 1899697, + chainWork = BigInt(12345), + bh = blockHeader) + + "get a block header" in { + val chainworkStr = { + val bytes = ByteVector(blockHeaderDb.chainWork.toByteArray) + val padded = if (bytes.length <= 32) { + bytes.padLeft(32) + } else bytes + + padded.toHex + } + + (mockChainApi + .getHeader(_: DoubleSha256DigestBE)) + .expects(blockHeader.hashBE) + .returning(Future.successful(Some(blockHeaderDb))) + + (mockChainApi + .getNumberOfConfirmations(_: DoubleSha256DigestBE)) + .expects(blockHeader.hashBE) + .returning(Future.successful(Some(1))) + + val route = + chainRoutes.handleCommand( + ServerCommand("getblockheader", Arr(Str(blockHeader.hashBE.hex)))) + + Get() ~> route ~> check { + assert(contentType == `application/json`) + assert(responseAs[ + String] == s"""{"result":{"raw":"${blockHeader.hex}","hash":"${blockHeader.hashBE.hex}","confirmations":1,"height":1899697,"version":${blockHeader.version.toLong},"versionHex":"${blockHeader.version.hex}","merkleroot":"${blockHeader.merkleRootHashBE.hex}","time":${blockHeader.time.toLong},"nonce":${blockHeader.nonce.toLong},"bits":"${blockHeader.nBits.hex}","difficulty":${blockHeader.difficulty.toDouble},"chainwork":"$chainworkStr","previousblockhash":"${blockHeader.previousBlockHashBE.hex}"},"error":null}""") + } + } + "return the wallet's balance" in { (mockWalletApi .getBalance()(_: ExecutionContext)) diff --git a/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala index 33ab40ebb0..cfddb661ab 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala @@ -7,6 +7,11 @@ import org.bitcoins.commons.jsonmodels.BitcoinSServerInfo import org.bitcoins.commons.serializers.Picklers._ import org.bitcoins.core.api.chain.ChainApi import org.bitcoins.core.config.BitcoinNetwork +import scodec.bits.ByteVector +import ujson._ + +import scala.concurrent.Future +import scala.util.{Failure, Success} case class ChainRoutes(chain: ChainApi, network: BitcoinNetwork)(implicit system: ActorSystem) @@ -39,6 +44,51 @@ case class ChainRoutes(chain: ChainApi, network: BitcoinNetwork)(implicit } } + case ServerCommand("getblockheader", arr) => + GetBlockHeader.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(GetBlockHeader(hash)) => + complete { + chain.getHeader(hash).flatMap { + case None => Future.successful(Server.httpSuccess(ujson.Null)) + case Some(header) => + chain.getNumberOfConfirmations(hash).map { + case None => + throw new RuntimeException( + s"Got unconfirmed header, ${header.hashBE.hex}") + case Some(confs) => + val chainworkStr = { + val bytes = ByteVector(header.chainWork.toByteArray) + val padded = if (bytes.length <= 32) { + bytes.padLeft(32) + } else bytes + + padded.toHex + } + + val json = Obj( + "raw" -> Str(header.blockHeader.hex), + "hash" -> Str(header.hashBE.hex), + "confirmations" -> Num(confs), + "height" -> Num(header.height), + "version" -> Num(header.version.toLong.toDouble), + "versionHex" -> Str(header.version.hex), + "merkleroot" -> Str(header.merkleRootHashBE.hex), + "time" -> Num(header.time.toBigInt.toDouble), + "nonce" -> Num(header.nonce.toBigInt.toDouble), + "bits" -> Str(header.nBits.hex), + "difficulty" -> Num(header.difficulty.toDouble), + "chainwork" -> Str(chainworkStr), + "previousblockhash" -> Str(header.previousBlockHashBE.hex) + ) + + Server.httpSuccess(json) + } + } + } + } + case ServerCommand("getinfo", _) => complete { chain.getBestBlockHeader().map { header => 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 05448833e7..59429a5abd 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -418,6 +418,19 @@ object ConvertToPSBT extends ServerJsonModels { } } +case class GetBlockHeader(hash: DoubleSha256DigestBE) + +object GetBlockHeader extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[GetBlockHeader] = + Try { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + GetBlockHeader(DoubleSha256DigestBE(jsArr.arr.head.str)) + } +} + case class DecodeRawTransaction(tx: Transaction) object DecodeRawTransaction extends ServerJsonModels { diff --git a/core/src/main/scala/org/bitcoins/core/api/chain/db/BlockHeaderDb.scala b/core/src/main/scala/org/bitcoins/core/api/chain/db/BlockHeaderDb.scala index 0f21cf3705..7eca37e520 100644 --- a/core/src/main/scala/org/bitcoins/core/api/chain/db/BlockHeaderDb.scala +++ b/core/src/main/scala/org/bitcoins/core/api/chain/db/BlockHeaderDb.scala @@ -28,6 +28,8 @@ case class BlockHeaderDb( blockHeader } + lazy val difficulty: BigInt = blockHeader.difficulty + lazy val hash: DoubleSha256Digest = hashBE.flip override def toString: String = {