From acc4ddba2d767f14799cd29d9ecbc7e031547c14 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 21 Apr 2018 18:00:38 -0500 Subject: [PATCH] server: Add getMasternodes method to XSNService --- .../xsn/explorer/models/rpc/Masternode.scala | 67 +++++++++++++++++++ .../xsn/explorer/services/XSNService.scala | 57 +++++++++++----- .../explorer/helpers/DummyXSNService.scala | 12 ++-- .../services/XSNServiceRPCImplSpec.scala | 67 ++++++++++++++++++- 4 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 server/app/com/xsn/explorer/models/rpc/Masternode.scala diff --git a/server/app/com/xsn/explorer/models/rpc/Masternode.scala b/server/app/com/xsn/explorer/models/rpc/Masternode.scala new file mode 100644 index 0000000..2c78348 --- /dev/null +++ b/server/app/com/xsn/explorer/models/rpc/Masternode.scala @@ -0,0 +1,67 @@ +package com.xsn.explorer.models.rpc + +import com.xsn.explorer.models.{Address, TransactionId} + +import scala.util.Try + +case class Masternode( + txid: TransactionId, + ip: String, + protocol: String, + status: String, + activeSeconds: Long, + lastSeen: Long, + payee: Address +) + +object Masternode { + + /** + * The RPC server give us a map like this one: + * + * ``` + * { + * "47b46ba99c760eeb6f443e5b6228d5dfeeac1cd5eec5fb9a79471af14c4c4c00-1": " ENABLED 70208 Xo27xzC57FonGesBDqyoqoFZ9kLhy946Be 1524348946 1146950 1524252156 63199 45.77.63.186:62583" + * } + * ``` + * + * The key is the transaction id used to send the funds to the masternode and the value a console formatted string with the values. + * Note that the transaction id ends with `-x` where x is a number. + */ + def fromMap(values: Map[String, String]): List[Masternode] = { + values + .map { case (key, value) => + val list = value.split(" ").map(_.trim).filter(_.nonEmpty).toList + parseValues(key, list) + } + .toList + .flatten + } + + private def parseTxid(key: String): Option[TransactionId] = { + key + .split("\\-") + .headOption + .flatMap(TransactionId.from) + } + + private def parseValues(key: String, values: List[String]): Option[Masternode] = values match { + case status :: protocol :: payee :: lastPaid :: activeSeconds :: lastSeen :: lastPaidBlock :: ip :: _ => + for { + txid <- parseTxid(key) + payee <- Address.from(payee) + lastSeen <- Try(lastSeen.toLong).toOption + activeSeconds <- Try(activeSeconds.toLong).toOption + } yield Masternode( + txid = txid, + status = status, + protocol = protocol, + payee = payee, + ip = ip, + lastSeen = lastSeen, + activeSeconds = activeSeconds + ) + + case _ => None + } +} diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index 51ee799..dfac66d 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -9,7 +9,6 @@ import com.xsn.explorer.config.RPCConfig import com.xsn.explorer.errors._ import com.xsn.explorer.executors.ExternalServiceExecutionContext import com.xsn.explorer.models._ -import com.xsn.explorer.models.rpc.{AddressBalance, Block, ServerStatistics, Transaction} import org.scalactic.{Bad, Good} import org.slf4j.LoggerFactory import play.api.libs.json.{JsNull, JsValue, Reads} @@ -19,19 +18,21 @@ import scala.util.Try trait XSNService { - def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] + def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction] - def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] + def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] - def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] + def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] - def getLatestBlock(): FutureApplicationResult[Block] + def getLatestBlock(): FutureApplicationResult[rpc.Block] - def getServerStatistics(): FutureApplicationResult[ServerStatistics] + def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] def getMasternodeCount(): FutureApplicationResult[Int] + + def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] } class XSNServiceRPCImpl @Inject() ( @@ -47,14 +48,14 @@ class XSNServiceRPCImpl @Inject() ( .withHttpHeaders("Content-Type" -> "text/plain") - override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { + override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction] = { val errorCodeMapper = Map(-5 -> TransactionNotFoundError) server .post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""") .map { response => - val maybe = getResult[Transaction](response, errorCodeMapper) + val maybe = getResult[rpc.Transaction](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}") @@ -63,7 +64,7 @@ class XSNServiceRPCImpl @Inject() ( } } - override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = { + override def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] = { val body = s""" |{ | "jsonrpc": "1.0", @@ -81,7 +82,7 @@ class XSNServiceRPCImpl @Inject() ( .post(body) .map { response => - val maybe = getResult[AddressBalance](response, errorCodeMapper) + val maybe = getResult[rpc.AddressBalance](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, address = ${address.string}, response = ${response.body}") @@ -117,7 +118,7 @@ class XSNServiceRPCImpl @Inject() ( } } - override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = { + override def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] = { val errorCodeMapper = Map(-5 -> BlockNotFoundError) val body = s"""{ "jsonrpc": "1.0", "method": "getblock", "params": ["${blockhash.string}"] }""" @@ -125,7 +126,7 @@ class XSNServiceRPCImpl @Inject() ( .post(body) .map { response => - val maybe = getResult[Block](response, errorCodeMapper) + val maybe = getResult[rpc.Block](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, txid = ${blockhash.string}, status = ${response.status}, response = ${response.body}") @@ -134,7 +135,7 @@ class XSNServiceRPCImpl @Inject() ( } } - override def getLatestBlock(): FutureApplicationResult[Block] = { + override def getLatestBlock(): FutureApplicationResult[rpc.Block] = { val body = s""" |{ | "jsonrpc": "1.0", @@ -163,7 +164,7 @@ class XSNServiceRPCImpl @Inject() ( } } - override def getServerStatistics(): FutureApplicationResult[ServerStatistics] = { + override def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] = { val body = s""" |{ | "jsonrpc": "1.0", @@ -175,7 +176,7 @@ class XSNServiceRPCImpl @Inject() ( server .post(body) .map { response => - val maybe = getResult[ServerStatistics](response) + val maybe = getResult[rpc.ServerStatistics](response) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}") @@ -205,6 +206,32 @@ class XSNServiceRPCImpl @Inject() ( } } + override def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] = { + val body = s""" + |{ + | "jsonrpc": "1.0", + | "method": "masternode", + | "params": ["list", "full"] + |} + |""".stripMargin + + server + .post(body) + .map { response => + val maybe = getResult[Map[String, String]](response) + .map { + case Good(map) => Good(rpc.Masternode.fromMap(map)) + 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/test/com/xsn/explorer/helpers/DummyXSNService.scala b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala index 78db732..d0fd1a0 100644 --- a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala +++ b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala @@ -2,16 +2,16 @@ package com.xsn.explorer.helpers import com.alexitc.playsonify.core.FutureApplicationResult import com.xsn.explorer.models._ -import com.xsn.explorer.models.rpc.{AddressBalance, Block, ServerStatistics, Transaction} import com.xsn.explorer.services.XSNService class DummyXSNService extends XSNService { - override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = ??? - override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = ??? + override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction] = ??? + override def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] = ??? override def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] = ??? - override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = ??? - override def getLatestBlock(): FutureApplicationResult[Block] = ??? - override def getServerStatistics(): FutureApplicationResult[ServerStatistics] = ??? + override def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] = ??? + override def getLatestBlock(): FutureApplicationResult[rpc.Block] = ??? + override def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] = ??? override def getMasternodeCount(): FutureApplicationResult[Int] = ??? + override def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] = ??? } diff --git a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala index f7003e1..ae23670 100644 --- a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala +++ b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala @@ -3,10 +3,11 @@ 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.{Address, Blockhash, Height} +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 +import org.scalactic.{Bad, Good} import org.scalatest.concurrent.ScalaFutures import org.scalatest.mockito.MockitoSugar import org.scalatest.{MustMatchers, OptionValues, WordSpec} @@ -345,4 +346,66 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures } } } + + "getMasternodeCount" should { + "return the count" in { + val content = "10" + + 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)) + + whenReady(service.getMasternodeCount()) { result => + result mustEqual Good(10) + } + } + } + + "getMasternodes" should { + "return the masternodes" in { + val content = + """ + |{ + | "c3efb8b60bda863a3a963d340901dc2b870e6ea51a34276a8f306d47ffb94f01-0": " WATCHDOG_EXPIRED 70208 XqdmM7rop8Sdgn8UjyNh3Povc3rhNSXYw2 1524349009 513323 1524297814 63936 45.77.136.212:62583", + | "b02f99d87194c9400ab147c070bf621770684906dedfbbe9ba5f3a35c26b8d01-1": " ENABLED 70208 XdNDRAiMUC9KiVRzhCTg9w44jQRdCpCRe3 1524349028 777344 1524312645 64183 45.32.148.13:62583" + |} + """.stripMargin + + val expected = List( + Masternode( + txid = TransactionId.from("c3efb8b60bda863a3a963d340901dc2b870e6ea51a34276a8f306d47ffb94f01").get, + ip = "45.77.136.212:62583", + protocol = "70208", + status = "WATCHDOG_EXPIRED", + activeSeconds = 513323, + lastSeen = 1524297814L, + Address.from("XqdmM7rop8Sdgn8UjyNh3Povc3rhNSXYw2").get), + + Masternode( + txid = TransactionId.from("b02f99d87194c9400ab147c070bf621770684906dedfbbe9ba5f3a35c26b8d01").get, + ip = "45.32.148.13:62583", + protocol = "70208", + status = "ENABLED", + activeSeconds = 777344, + lastSeen = 1524312645L, + 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)) + + whenReady(service.getMasternodes()) { result => + result.isGood mustEqual true + + val masternodes = result.get + masternodes mustEqual expected + } + } + } }