From f070f7d2d73ab28681b390d9c8cdeefedf89a88f Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Wed, 14 Mar 2018 21:14:18 -0600 Subject: [PATCH] server: Add getAddressBalance method to XSNService --- .../xsn/explorer/errors/addressErrors.scala | 15 ++++ .../xsn/explorer/services/XSNService.scala | 82 +++++++++++++------ server/conf/messages | 2 + .../services/XSNServiceRPCImplSpec.scala | 52 +++++++++++- .../TransactionsControllerSpec.scala | 2 + 5 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 server/app/com/xsn/explorer/errors/addressErrors.scala diff --git a/server/app/com/xsn/explorer/errors/addressErrors.scala b/server/app/com/xsn/explorer/errors/addressErrors.scala new file mode 100644 index 0000000..f90eedc --- /dev/null +++ b/server/app/com/xsn/explorer/errors/addressErrors.scala @@ -0,0 +1,15 @@ +package com.xsn.explorer.errors + +import com.alexitc.playsonify.models.{FieldValidationError, InputValidationError, PublicError} +import play.api.i18n.{Lang, MessagesApi} + +sealed trait AddressError + +case object AddressFormatError extends AddressError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.address.format") + val error = FieldValidationError("address", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index 3ffb219..f0fcc2f 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -2,22 +2,24 @@ package com.xsn.explorer.services import javax.inject.Inject -import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} import com.alexitc.playsonify.models.ApplicationError import com.xsn.explorer.config.RPCConfig -import com.xsn.explorer.errors.{TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} +import com.xsn.explorer.errors.{AddressFormatError, TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} import com.xsn.explorer.executors.ExternalServiceExecutionContext -import com.xsn.explorer.models.{Transaction, TransactionId} +import com.xsn.explorer.models.{Address, AddressBalance, Transaction, TransactionId} import org.scalactic.{Bad, Good} import org.slf4j.LoggerFactory -import play.api.libs.json.{JsNull, JsValue} -import play.api.libs.ws.{WSAuthScheme, WSClient} +import play.api.libs.json.{JsNull, JsValue, Reads} +import play.api.libs.ws.{WSAuthScheme, WSClient, WSResponse} import scala.util.Try trait XSNService { def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] + + def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] } class XSNServiceRPCImpl @Inject() ( @@ -32,25 +34,15 @@ class XSNServiceRPCImpl @Inject() ( .withAuth(rpcConfig.username.string, rpcConfig.password.string, WSAuthScheme.BASIC) .withHttpHeaders("Content-Type" -> "text/plain") + override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { + val errorCodeMapper = Map(-5 -> TransactionNotFoundError) + server .post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""") .map { response => - val maybe = Option(response) - .filter(_.status == 200) - .flatMap { r => Try(r.json).toOption } - .flatMap { json => - (json \ "result") - .asOpt[Transaction] - .map { Good(_) } - .orElse { - mapError(json) - .map(Bad.apply) - .map(_.accumulating) - } - } - + val maybe = getResult[Transaction](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}") @@ -59,7 +51,34 @@ class XSNServiceRPCImpl @Inject() ( } } - private def mapError(json: JsValue): Option[ApplicationError] = { + override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = { + val body = s""" + |{ + | "jsonrpc": "1.0", + | "method": "getaddressbalance", + | "params": [ + | { "addresses": ["${address.string}"] } + | ] + |} + |""".stripMargin + + // the network returns 0 for valid addresses + val errorCodeMapper = Map(-5 -> AddressFormatError) + + server + .post(body) + .map { response => + + val maybe = getResult[AddressBalance](response, errorCodeMapper) + maybe.getOrElse { + logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, address = ${address.string}, response = ${response.body}") + + Bad(XSNUnexpectedResponseError).accumulating + } + } + } + + private def mapError(json: JsValue, errorCodeMapper: Map[Int, ApplicationError]): Option[ApplicationError] = { val jsonErrorMaybe = (json \ "error") .asOpt[JsValue] .filter(_ != JsNull) @@ -69,7 +88,7 @@ class XSNServiceRPCImpl @Inject() ( // from error code if possible (jsonError \ "code") .asOpt[Int] - .flatMap(fromErrorCode) + .flatMap(errorCodeMapper.get) .orElse { // from message (jsonError \ "message") @@ -80,8 +99,23 @@ class XSNServiceRPCImpl @Inject() ( } } - private def fromErrorCode(code: Int): Option[ApplicationError] = code match { - case -5 => Some(TransactionNotFoundError) - case _ => None + private def getResult[A]( + response: WSResponse, + errorCodeMapper: Map[Int, ApplicationError] = Map.empty)( + implicit reads: Reads[A]): Option[ApplicationResult[A]] = { + + Option(response) + .filter(_.status == 200) + .flatMap { r => Try(r.json).toOption } + .flatMap { json => + (json \ "result") + .asOpt[A] + .map { Good(_) } + .orElse { + mapError(json, errorCodeMapper) + .map(Bad.apply) + .map(_.accumulating) + } + } } } diff --git a/server/conf/messages b/server/conf/messages index 299629c..337960c 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -4,3 +4,5 @@ xsn.server.unexpectedError=Unexpected error from the XSN network error.transaction.format=Invalid transaction format error.transaction.notFound=Transaction not found + +error.address.format=Invalid address format diff --git a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala index 339a9bf..a62e86e 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.{TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} +import com.xsn.explorer.errors.{AddressFormatError, TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError} import com.xsn.explorer.helpers.Executors -import com.xsn.explorer.models.TransactionId +import com.xsn.explorer.models.{Address, TransactionId} import org.mockito.ArgumentMatchers._ import org.mockito.Mockito._ import org.scalactic.Bad @@ -230,4 +230,52 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures } } } + + "getAddressBalance" should { + "return the balance" in { + val responseBody = + """ + |{ + | "result": { + | "balance": 24650100000000, + | "received": 1060950100000000 + | }, + | "error": null, + | "id": null + |} + """.stripMargin.trim + + val address = Address.from("Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw").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.getAddressBalance(address)) { result => + result.isGood mustEqual true + + val balance = result.get + balance.balance mustEqual BigInt("24650100000000") + balance.received mustEqual BigInt("1060950100000000") + } + } + + "fail on invalid address" in { + val responseBody = """{"result":null,"error":{"code":-5,"message":"Invalid address"},"id":null}""" + + val address = Address.from("Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLW").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.getAddressBalance(address)) { result => + result mustEqual Bad(AddressFormatError).accumulating + } + } + } } diff --git a/server/test/controllers/TransactionsControllerSpec.scala b/server/test/controllers/TransactionsControllerSpec.scala index acfd1b3..5aa008b 100644 --- a/server/test/controllers/TransactionsControllerSpec.scala +++ b/server/test/controllers/TransactionsControllerSpec.scala @@ -71,6 +71,8 @@ class TransactionsControllerSpec extends MyAPISpec { Future.successful(result) } + + override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = ??? } override val application = guiceApplicationBuilder