diff --git a/server/app/com/xsn/explorer/errors/blockErrors.scala b/server/app/com/xsn/explorer/errors/blockErrors.scala new file mode 100644 index 0000000..38768af --- /dev/null +++ b/server/app/com/xsn/explorer/errors/blockErrors.scala @@ -0,0 +1,24 @@ +package com.xsn.explorer.errors + +import com.alexitc.playsonify.models.{FieldValidationError, InputValidationError, PublicError} +import play.api.i18n.{Lang, MessagesApi} + +sealed trait BlockError + +case object BlockhashFormatError extends BlockError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.block.format") + val error = FieldValidationError("blockhash", message) + List(error) + } +} + +case object BlockNotFoundError extends BlockError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.block.notFound") + val error = FieldValidationError("blockhash", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index 7f15d56..138ad78 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -5,9 +5,9 @@ import javax.inject.Inject import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} import com.alexitc.playsonify.models.ApplicationError import com.xsn.explorer.config.RPCConfig -import com.xsn.explorer.errors.{AddressFormatError, TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} +import com.xsn.explorer.errors._ import com.xsn.explorer.executors.ExternalServiceExecutionContext -import com.xsn.explorer.models.{Address, AddressBalance, Transaction, TransactionId} +import com.xsn.explorer.models._ import org.scalactic.{Bad, Good} import org.slf4j.LoggerFactory import play.api.libs.json.{JsNull, JsValue, Reads} @@ -22,6 +22,8 @@ trait XSNService { def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] def getTransactionCount(address: Address): FutureApplicationResult[Int] + + def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] } class XSNServiceRPCImpl @Inject() ( @@ -107,6 +109,23 @@ class XSNServiceRPCImpl @Inject() ( } } + override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = { + val errorCodeMapper = Map(-5 -> BlockNotFoundError) + val body = s"""{ "jsonrpc": "1.0", "method": "getblock", "params": ["${blockhash.string}"] }""" + + server + .post(body) + .map { response => + + val maybe = getResult[Block](response, errorCodeMapper) + maybe.getOrElse { + logger.warn(s"Unexpected response from XSN Server, txid = ${blockhash.string}, status = ${response.status}, response = ${response.body}") + + Bad(XSNUnexpectedResponseError).accumulating + } + } + } + private def mapError(json: JsValue, errorCodeMapper: Map[Int, ApplicationError]): Option[ApplicationError] = { val jsonErrorMaybe = (json \ "error") .asOpt[JsValue] diff --git a/server/conf/messages b/server/conf/messages index 337960c..873e2d0 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -6,3 +6,6 @@ error.transaction.format=Invalid transaction format error.transaction.notFound=Transaction not found error.address.format=Invalid address format + +error.block.format=Invalid blockhash format +error.block.notFound=Block not found diff --git a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala index f249104..a1ff2db 100644 --- a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala +++ b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala @@ -1,7 +1,7 @@ package com.xsn.explorer.helpers import com.alexitc.playsonify.core.FutureApplicationResult -import com.xsn.explorer.models.{Address, AddressBalance, Transaction, TransactionId} +import com.xsn.explorer.models._ import com.xsn.explorer.services.XSNService class DummyXSNService extends XSNService { @@ -9,4 +9,5 @@ class DummyXSNService extends XSNService { override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = ??? override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = ??? override def getTransactionCount(address: Address): FutureApplicationResult[Int] = ??? + override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = ??? } diff --git a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala index e083a9a..76e35b9 100644 --- a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala +++ b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala @@ -1,9 +1,9 @@ package com.xsn.explorer.services import com.xsn.explorer.config.RPCConfig -import com.xsn.explorer.errors.{AddressFormatError, TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} +import com.xsn.explorer.errors._ import com.xsn.explorer.helpers.Executors -import com.xsn.explorer.models.{Address, TransactionId} +import com.xsn.explorer.models.{Address, Blockhash, TransactionId} import org.mockito.ArgumentMatchers._ import org.mockito.Mockito._ import org.scalactic.{Bad, Good} @@ -329,4 +329,68 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures } } } + + "getBlock" should { + "return a block" in { + val responseBody = + """ + |{ + | "result": { + | "hash": "b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81", + | "confirmations": 11163, + | "size": 478, + | "height": 809, + | "version": 536870912, + | "merkleroot": "598cc6ba8238d87641b0dbd02485b7d635b5417429df3145c98c3ff8779ab4b8", + | "tx": [ + | "7f12adbb63d443502cf151c76946d5faa0b1c662a5d67afc7da085c74e06f1ce", + | "0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641" + | ], + | "time": 1520318120, + | "mediantime": 1520318054, + | "nonce": 0, + | "bits": "1d011212", + | "difficulty": 0.9340526210769362, + | "chainwork": "00000000000000000000000000000000000000000000000000000084c71ff420", + | "previousblockhash": "9490ce5d14bb5e79a790ddede03fc3a9bde3f7156f34a57ae3ceb56ae7426c14", + | "nextblockhash": "f6e3199c241131e79640fe027a6ef993c02b3520c3d4ba08cd67abfbb98ec07e" + | }, + | "error": null, + | "id": null + |} + |""".stripMargin + + val blockhash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81").get + + val json = Json.parse(responseBody) + + when(response.status).thenReturn(200) + when(response.json).thenReturn(json) + when(request.post[String](anyString())(any())).thenReturn(Future.successful(response)) + + whenReady(service.getBlock(blockhash)) { result => + result.isGood mustEqual true + + val block = result.get + block.hash.string mustEqual "b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81" + block.transactions.size mustEqual 2 + } + } + + "fail on unknown block" in { + val responseBody = """{"result":null,"error":{"code":-5,"message":"Block not found"},"id":null}""" + + val blockhash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b80").get + + val json = Json.parse(responseBody) + + when(response.status).thenReturn(200) + when(response.json).thenReturn(json) + when(request.post[String](anyString())(any())).thenReturn(Future.successful(response)) + + whenReady(service.getBlock(blockhash)) { result => + result mustEqual Bad(BlockNotFoundError).accumulating + } + } + } }