From 8ecc447f222e4b405820c9b0237463ac40191fad Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 17 Mar 2018 14:34:35 -0600 Subject: [PATCH] server: Add support for retrieving PoW blocks --- .../com/xsn/explorer/errors/blockErrors.scala | 9 ++ .../xsn/explorer/models/BlockRewards.scala | 9 -- .../xsn/explorer/models/blockRewards.scala | 15 ++++ .../xsn/explorer/services/BlockService.scala | 45 +++++++++- .../explorer/services/logic/BlockLogic.scala | 12 ++- .../services/logic/TransactionLogic.scala | 6 +- server/conf/messages | 1 + .../controllers/BlocksControllerSpec.scala | 82 ++++++++++++++++++- 8 files changed, 158 insertions(+), 21 deletions(-) delete mode 100644 server/app/com/xsn/explorer/models/BlockRewards.scala create mode 100644 server/app/com/xsn/explorer/models/blockRewards.scala diff --git a/server/app/com/xsn/explorer/errors/blockErrors.scala b/server/app/com/xsn/explorer/errors/blockErrors.scala index 38768af..fb1f4e3 100644 --- a/server/app/com/xsn/explorer/errors/blockErrors.scala +++ b/server/app/com/xsn/explorer/errors/blockErrors.scala @@ -22,3 +22,12 @@ case object BlockNotFoundError extends BlockError with InputValidationError { List(error) } } + +case object TPoSBlockNotSupportedError extends BlockError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.block.tposUnsupported") + val error = FieldValidationError("blockhash", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/models/BlockRewards.scala b/server/app/com/xsn/explorer/models/BlockRewards.scala deleted file mode 100644 index ff0857c..0000000 --- a/server/app/com/xsn/explorer/models/BlockRewards.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.xsn.explorer.models - -import play.api.libs.json.{Json, Writes} - -case class BlockRewards(coinstake: BlockReward, masternode: Option[BlockReward]) - -object BlockRewards { - implicit val writes: Writes[BlockRewards] = Json.writes[BlockRewards] -} diff --git a/server/app/com/xsn/explorer/models/blockRewards.scala b/server/app/com/xsn/explorer/models/blockRewards.scala new file mode 100644 index 0000000..585f887 --- /dev/null +++ b/server/app/com/xsn/explorer/models/blockRewards.scala @@ -0,0 +1,15 @@ +package com.xsn.explorer.models + +import play.api.libs.json.{Json, Writes} + +sealed trait BlockRewards + +object BlockRewards { + implicit val writes: Writes[BlockRewards] = Writes[BlockRewards] { + case r: PoWBlockRewards => Json.writes[PoWBlockRewards].writes(r) + case r: PoSBlockRewards => Json.writes[PoSBlockRewards].writes(r) + } +} + +case class PoWBlockRewards(reward: BlockReward) extends BlockRewards +case class PoSBlockRewards(coinstake: BlockReward, masternode: Option[BlockReward]) extends BlockRewards diff --git a/server/app/com/xsn/explorer/services/BlockService.scala b/server/app/com/xsn/explorer/services/BlockService.scala index 0a34748..2a578bd 100644 --- a/server/app/com/xsn/explorer/services/BlockService.scala +++ b/server/app/com/xsn/explorer/services/BlockService.scala @@ -4,11 +4,12 @@ import javax.inject.Inject import com.alexitc.playsonify.core.FutureApplicationResult import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} -import com.xsn.explorer.errors.BlockNotFoundError +import com.xsn.explorer.errors.{BlockNotFoundError, TPoSBlockNotSupportedError} import com.xsn.explorer.models._ import com.xsn.explorer.services.logic.{BlockLogic, TransactionLogic} +import org.scalactic.Bad -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} class BlockService @Inject() ( xsnService: XSNService, @@ -25,6 +26,36 @@ class BlockService @Inject() ( .getBlock(blockhash) .toFutureOr + rewards <- getBlockRewards(block).toFutureOr + } yield BlockDetails(block, rewards) + + result.toFuture + } + + private def getBlockRewards(block: Block): FutureApplicationResult[BlockRewards] = { + if (block.isPoW) { + getPoWBlockRewards(block) + } else if (block.isPoS) { + getPoSBlockRewards(block) + } else { + getTPoSBlockRewards(block) + } + } + + private def getPoWBlockRewards(block: Block): FutureApplicationResult[PoWBlockRewards] = { + val result = for { + txid <- blockLogic.getPoWTransactionId(block).toFutureOr + // TODO: handle tx not found + tx <- xsnService.getTransaction(txid).toFutureOr + vout <- transactionLogic.getVOUT(0, tx, BlockNotFoundError).toFutureOr + address <- transactionLogic.getAddress(vout, BlockNotFoundError).toFutureOr + } yield PoWBlockRewards(BlockReward(address, vout.value)) + + result.toFuture + } + + private def getPoSBlockRewards(block: Block): FutureApplicationResult[PoSBlockRewards] = { + val result = for { coinstakeTxId <- blockLogic .getCoinstakeTransactionId(block) .toFutureOr @@ -47,10 +78,16 @@ class BlockService @Inject() ( .toFutureOr rewards <- blockLogic - .getRewards(coinstakeTx, coinstakeAddress, previousToCoinstakeVOUT.value) + .getPoSRewards(coinstakeTx, coinstakeAddress, previousToCoinstakeVOUT.value) .toFutureOr - } yield BlockDetails(block, rewards) + } yield rewards result.toFuture } + + // TODO: Complete it + private def getTPoSBlockRewards(block: Block): FutureApplicationResult[BlockRewards] = { + val result = Bad(TPoSBlockNotSupportedError).accumulating + Future.successful(result) + } } diff --git a/server/app/com/xsn/explorer/services/logic/BlockLogic.scala b/server/app/com/xsn/explorer/services/logic/BlockLogic.scala index 1b2c829..b649411 100644 --- a/server/app/com/xsn/explorer/services/logic/BlockLogic.scala +++ b/server/app/com/xsn/explorer/services/logic/BlockLogic.scala @@ -12,6 +12,12 @@ class BlockLogic { Or.from(maybe, One(BlockhashFormatError)) } + def getPoWTransactionId(block: Block): ApplicationResult[TransactionId] = { + val maybe = block.transactions.headOption + + Or.from(maybe, One(BlockNotFoundError)) + } + /** * Get the coinstake transaction id for the given block. * @@ -43,10 +49,10 @@ class BlockLogic { * Sometimes there could be rounding errors, for example, when the input is not exactly divisible by 2, * we return 0 in that case because the reward could be negative. */ - def getRewards( + def getPoSRewards( coinstakeTx: Transaction, coinstakeAddress: Address, - coinstakeInput: BigDecimal): ApplicationResult[BlockRewards] = { + coinstakeInput: BigDecimal): ApplicationResult[PoSBlockRewards] = { // first vout is empty, useless val coinstakeVOUT = coinstakeTx.vout.drop(1) @@ -69,7 +75,7 @@ class BlockLogic { ) } - Good(BlockRewards(coinstakeReward, masternodeRewardMaybe)) + Good(PoSBlockRewards(coinstakeReward, masternodeRewardMaybe)) } else { Bad(BlockNotFoundError).accumulating } diff --git a/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala b/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala index 2ed93d6..1d99e51 100644 --- a/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala +++ b/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala @@ -18,7 +18,11 @@ class TransactionLogic { } def getVOUT(vin: TransactionVIN, previousTX: Transaction, error: ApplicationError): ApplicationResult[TransactionVOUT] = { - val maybe = previousTX.vout.find(_.n == vin.voutIndex) + getVOUT(vin.voutIndex, previousTX, error) + } + + def getVOUT(index: Int, previousTX: Transaction, error: ApplicationError): ApplicationResult[TransactionVOUT] = { + val maybe = previousTX.vout.find(_.n == index) Or.from(maybe, One(error)) } } diff --git a/server/conf/messages b/server/conf/messages index 873e2d0..740ac5f 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -9,3 +9,4 @@ error.address.format=Invalid address format error.block.format=Invalid blockhash format error.block.notFound=Block not found +error.block.tposUnsupported=TPoS Blocks are not supported at the moment diff --git a/server/test/controllers/BlocksControllerSpec.scala b/server/test/controllers/BlocksControllerSpec.scala index 03cc5b5..15471a5 100644 --- a/server/test/controllers/BlocksControllerSpec.scala +++ b/server/test/controllers/BlocksControllerSpec.scala @@ -75,10 +75,34 @@ class BlocksControllerSpec extends MyAPISpec { ) ) + // TPoS + val tposBlock = posBlock.copy( + hash = Blockhash.from("c6944a33e3e03eb0ccd350f1fc2d6e5f3bd1411e1efddc0990aa3243663b41b7").get, + tposContract = Some("7f2b5f25b0ae24a417633e4214827f930a69802c1c43d1fb2ff7b7075b2d1701")) + + // PoW + val powBlock = posBlock.copy( + hash = Blockhash.from("000004645e2717b556682e3c642a4c6e473bf25c653ff8e8c114a3006040ffb8").get, + transactions = List( + TransactionId.from("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8").get + ), + height = Height(2) + ) + + val powBlockPreviousTx = createTx( + id = TransactionId.from("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8").get, + vin = None, + vout = List( + TransactionVOUT(BigDecimal("76500000.00000000"), 0, "pubkey", Some(Address.from("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9").get)) + ) + ) + val customXSNService = new DummyXSNService { val blocks = Map( posBlock.hash -> posBlock, - posBlockRoundingError.hash -> posBlockRoundingError + posBlockRoundingError.hash -> posBlockRoundingError, + tposBlock.hash -> tposBlock, + powBlock.hash -> powBlock ) override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = { @@ -91,12 +115,12 @@ class BlocksControllerSpec extends MyAPISpec { Future.successful(result) } - val txs = Map( posBlockCoinstakeTx.id -> posBlockCoinstakeTx, posBlockCoinstakeTxInput.id -> posBlockCoinstakeTxInput, posBlockRoundingErrorCoinstakeTx.id -> posBlockRoundingErrorCoinstakeTx, - posBlockRoundingErrorCoinstakeTxInput.id -> posBlockRoundingErrorCoinstakeTxInput + posBlockRoundingErrorCoinstakeTxInput.id -> posBlockRoundingErrorCoinstakeTxInput, + powBlockPreviousTx.id -> powBlockPreviousTx ) override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { @@ -184,6 +208,36 @@ class BlocksControllerSpec extends MyAPISpec { jsonMasternode.isEmpty mustEqual true } + "retrieve a PoW block" in { + val block = powBlock + val response = GET(url(block.hash.string)) + + status(response) mustEqual OK + + val json = contentAsJson(response) + val jsonBlock = (json \ "block").as[JsValue] + val jsonRewards = (json \ "rewards").as[JsValue] + + (jsonBlock \ "hash").as[Blockhash] mustEqual block.hash + (jsonBlock \ "size").as[Size] mustEqual block.size + (jsonBlock \ "bits").as[String] mustEqual block.bits + (jsonBlock \ "chainwork").as[String] mustEqual block.chainwork + (jsonBlock \ "difficulty").as[BigDecimal] mustEqual block.difficulty + (jsonBlock \ "confirmations").as[Confirmations] mustEqual block.confirmations + (jsonBlock \ "height").as[Height] mustEqual block.height + (jsonBlock \ "medianTime").as[Long] mustEqual block.medianTime + (jsonBlock \ "time").as[Long] mustEqual block.time + (jsonBlock \ "merkleRoot").as[Blockhash] mustEqual block.merkleRoot + (jsonBlock \ "version").as[Long] mustEqual block.version + (jsonBlock \ "nonce").as[Int] mustEqual block.nonce + (jsonBlock \ "previousBlockhash").asOpt[Blockhash] mustEqual block.previousBlockhash + (jsonBlock \ "nextBlockhash").asOpt[Blockhash] mustEqual block.nextBlockhash + + val jsonReward = (jsonRewards \ "reward").as[JsValue] + (jsonReward \ "address").as[String] mustEqual "XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9" + (jsonReward \ "value").as[BigDecimal] mustEqual BigDecimal("76500000") + } + "fail on the wrong blockhash format" in { val response = GET(url("000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5")) @@ -215,9 +269,29 @@ class BlocksControllerSpec extends MyAPISpec { (error \ "field").as[String] mustEqual "blockhash" (error \ "message").as[String].nonEmpty mustEqual true } + + "fail on TPoS block" in { + val response = GET(url("c6944a33e3e03eb0ccd350f1fc2d6e5f3bd1411e1efddc0990aa3243663b41b7")) + + status(response) mustEqual BAD_REQUEST + + val json = contentAsJson(response) + val errorList = (json \ "errors").as[List[JsValue]] + + errorList.size mustEqual 1 + val error = errorList.head + + (error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType + (error \ "field").as[String] mustEqual "blockhash" + (error \ "message").as[String].nonEmpty mustEqual true + } } private def createTx(id: TransactionId, vin: TransactionVIN, vout: List[TransactionVOUT]): Transaction = { + createTx(id, Some(vin), vout) + } + + private def createTx(id: TransactionId, vin: Option[TransactionVIN], vout: List[TransactionVOUT]): Transaction = { Transaction( id = id, size = Size(234), @@ -225,7 +299,7 @@ class BlocksControllerSpec extends MyAPISpec { time = 1520318120, blocktime = 1520318120, confirmations = Confirmations(1950), - vin = Some(vin), + vin = vin, vout = vout ) }