diff --git a/server/app/com/xsn/explorer/errors/masternodeErrors.scala b/server/app/com/xsn/explorer/errors/masternodeErrors.scala new file mode 100644 index 0000000..c5f53e3 --- /dev/null +++ b/server/app/com/xsn/explorer/errors/masternodeErrors.scala @@ -0,0 +1,15 @@ +package com.xsn.explorer.errors + +import com.alexitc.playsonify.models.{FieldValidationError, NotFoundError, PublicError} +import play.api.i18n.{Lang, MessagesApi} + +trait MasternodeError + +case object MasternodeNotFoundError extends MasternodeError with NotFoundError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.masternode.notFound") + val error = FieldValidationError("ip", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/models/IPAddress.scala b/server/app/com/xsn/explorer/models/IPAddress.scala new file mode 100644 index 0000000..a9a5427 --- /dev/null +++ b/server/app/com/xsn/explorer/models/IPAddress.scala @@ -0,0 +1,17 @@ +package com.xsn.explorer.models + +import com.alexitc.playsonify.models.WrappedString + +class IPAddress (val string: String) extends WrappedString + +object IPAddress { + private val pattern = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$".r.pattern + + def from(string: String): Option[IPAddress] = { + if (pattern.matcher(string).matches()) { + Some(new IPAddress(string)) + } else { + None + } + } +} diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index dfac66d..88dc3ef 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -33,6 +33,8 @@ trait XSNService { def getMasternodeCount(): FutureApplicationResult[Int] def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] + + def getMasternode(ipAddress: IPAddress): FutureApplicationResult[rpc.Masternode] } class XSNServiceRPCImpl @Inject() ( @@ -232,6 +234,38 @@ class XSNServiceRPCImpl @Inject() ( } } + override def getMasternode(ipAddress: IPAddress): FutureApplicationResult[rpc.Masternode] = { + val body = s""" + |{ + | "jsonrpc": "1.0", + | "method": "masternode", + | "params": ["list", "full", "${ipAddress.string}"] + |} + |""".stripMargin + + server + .post(body) + .map { response => + val maybe = getResult[Map[String, String]](response) + .map { + case Good(map) => + rpc.Masternode + .fromMap(map) + .headOption + .map(Good(_)) + .getOrElse(Bad(MasternodeNotFoundError).accumulating) + + case Bad(errors) => Bad(errors) + } + + maybe.getOrElse { + logger.warn(s"Unexpected response from XSN Server, 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 b078e70..adbc3b9 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -10,6 +10,8 @@ error.address.format=Invalid address format error.block.format=Invalid blockhash format error.block.notFound=Block not found +error.masternode.notFound=Masternode not found + error.paginatedQuery.offset.invalid=Invalid offset, it should be a number greater or equal than 0 error.paginatedQuery.limit.invalid=Invalid limit, it should be a number between 1 and and {0} diff --git a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala index d0fd1a0..ac4ca86 100644 --- a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala +++ b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala @@ -2,6 +2,7 @@ package com.xsn.explorer.helpers import com.alexitc.playsonify.core.FutureApplicationResult import com.xsn.explorer.models._ +import com.xsn.explorer.models.rpc.Masternode import com.xsn.explorer.services.XSNService class DummyXSNService extends XSNService { @@ -14,4 +15,5 @@ class DummyXSNService extends XSNService { override def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] = ??? override def getMasternodeCount(): FutureApplicationResult[Int] = ??? override def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] = ??? + override def getMasternode(ipAddress: IPAddress): FutureApplicationResult[Masternode] = ??? } diff --git a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala index b8e383d..79b9f35 100644 --- a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala +++ b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala @@ -3,8 +3,8 @@ package com.xsn.explorer.services import com.xsn.explorer.config.RPCConfig import com.xsn.explorer.errors._ import com.xsn.explorer.helpers.{BlockLoader, DataHelper, Executors, TransactionLoader} +import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.Masternode -import com.xsn.explorer.models.{Address, Blockhash, Height, TransactionId} import org.mockito.ArgumentMatchers._ import org.mockito.Mockito._ import org.scalactic.{Bad, Good} @@ -408,4 +408,55 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures } } } + + "getMasternode" should { + "return the masternode" in { + val content = + """ + |{ + | "b02f99d87194c9400ab147c070bf621770684906dedfbbe9ba5f3a35c26b8d01-1": " ENABLED 70208 XdNDRAiMUC9KiVRzhCTg9w44jQRdCpCRe3 1524349028 777344 1524312645 64183 45.32.148.13:62583" + |} + """.stripMargin + + val expected = Masternode( + txid = TransactionId.from("b02f99d87194c9400ab147c070bf621770684906dedfbbe9ba5f3a35c26b8d01").get, + ip = "45.32.148.13:62583", + protocol = "70208", + status = "ENABLED", + activeSeconds = 777344, + lastSeen = 1524349028, + Address.from("XdNDRAiMUC9KiVRzhCTg9w44jQRdCpCRe3").get) + + val responseBody = createRPCSuccessfulResponse(Json.parse(content)) + 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)) + + val ip = IPAddress.from("45.32.148.13").get + whenReady(service.getMasternode(ip)) { result => + result mustEqual Good(expected) + } + } + + "fail when the masternode is not found" in { + val content = + """ + |{} + """.stripMargin + + val responseBody = createRPCSuccessfulResponse(Json.parse(content)) + 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)) + + val ip = IPAddress.from("45.32.148.13").get + whenReady(service.getMasternode(ip)) { result => + result mustEqual Bad(MasternodeNotFoundError).accumulating + } + } + } }