Browse Source

server: Add getAddressBalance method to XSNService

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
f070f7d2d7
  1. 15
      server/app/com/xsn/explorer/errors/addressErrors.scala
  2. 82
      server/app/com/xsn/explorer/services/XSNService.scala
  3. 2
      server/conf/messages
  4. 52
      server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala
  5. 2
      server/test/controllers/TransactionsControllerSpec.scala

15
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)
}
}

82
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)
}
}
}
}

2
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

52
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
}
}
}
}

2
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

Loading…
Cancel
Save