Browse Source

server: Add XSNService

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
da63a71c0c
  1. 11
      server/app/com/xsn/explorer/modules/ExecutorsModule.scala
  2. 11
      server/app/com/xsn/explorer/modules/XSNServiceModule.scala
  3. 88
      server/app/com/xsn/explorer/services/XSNService.scala
  4. 2
      server/conf/application.conf
  5. 131
      server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala

11
server/app/com/xsn/explorer/modules/ExecutorsModule.scala

@ -0,0 +1,11 @@
package com.xsn.explorer.modules
import com.google.inject.AbstractModule
import com.xsn.explorer.executors.{ExternalServiceAkkaExecutionContext, ExternalServiceExecutionContext}
class ExecutorsModule extends AbstractModule {
override def configure(): Unit = {
bind(classOf[ExternalServiceExecutionContext]).to(classOf[ExternalServiceAkkaExecutionContext])
}
}

11
server/app/com/xsn/explorer/modules/XSNServiceModule.scala

@ -0,0 +1,11 @@
package com.xsn.explorer.modules
import com.google.inject.AbstractModule
import com.xsn.explorer.services.{XSNService, XSNServiceRPCImpl}
class XSNServiceModule extends AbstractModule {
override def configure(): Unit = {
bind(classOf[XSNService]).to(classOf[XSNServiceRPCImpl])
}
}

88
server/app/com/xsn/explorer/services/XSNService.scala

@ -0,0 +1,88 @@
package com.xsn.explorer.services
import javax.inject.Inject
import com.alexitc.playsonify.core.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.executors.ExternalServiceExecutionContext
import com.xsn.explorer.models.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 scala.util.Try
trait XSNService {
def getTransaction(txid: TransactionId): FutureApplicationResult[JsValue]
}
class XSNServiceRPCImpl @Inject() (
ws: WSClient,
rpcConfig: RPCConfig)(
implicit ec: ExternalServiceExecutionContext)
extends XSNService {
private val logger = LoggerFactory.getLogger(this.getClass)
private val server = ws.url(rpcConfig.host.string)
.withAuth(rpcConfig.username.string, rpcConfig.password.string, WSAuthScheme.BASIC)
.withHttpHeaders("Content-Type" -> "text/plain")
// TODO: cache successful results
override def getTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = {
server
.post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""")
.map { response =>
val maybe = Try(response.json)
.toOption
.flatMap { json =>
(json \ "result")
.asOpt[JsValue]
.filter(_ != JsNull)
.map { Good(_) }
.orElse {
mapError(json)
.map(Bad.apply)
.map(_.accumulating)
}
}
maybe.getOrElse {
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, response = ${response.body}")
Bad(XSNUnexpectedResponseError).accumulating
}
}
}
private def mapError(json: JsValue): Option[ApplicationError] = {
val jsonErrorMaybe = (json \ "error")
.asOpt[JsValue]
.filter(_ != JsNull)
jsonErrorMaybe
.flatMap { jsonError =>
// from error code if possible
(jsonError \ "code")
.asOpt[Int]
.flatMap(fromErrorCode)
.orElse {
// from message
(jsonError \ "message")
.asOpt[String]
.filter(_.nonEmpty)
.map(XSNMessageError.apply)
}
}
}
private def fromErrorCode(code: Int): Option[ApplicationError] = code match {
case -5 => Some(TransactionNotFoundError)
case _ => None
}
}

2
server/conf/application.conf

@ -24,6 +24,8 @@ rpc {
}
play.modules.enabled += "com.xsn.explorer.modules.ConfigModule"
play.modules.enabled += "com.xsn.explorer.modules.ExecutorsModule"
play.modules.enabled += "com.xsn.explorer.modules.XSNServiceModule"
externalService.dispatcher {
executor = "thread-pool-executor"

131
server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala

@ -0,0 +1,131 @@
package com.xsn.explorer.services
import com.xsn.explorer.config.RPCConfig
import com.xsn.explorer.errors.{TransactionNotFoundError, XSNMessageError, XSNUnexpectedResponseError}
import com.xsn.explorer.helpers.Executors
import com.xsn.explorer.models.TransactionId
import org.mockito.ArgumentMatchers._
import org.mockito.Mockito._
import org.scalactic.Bad
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.mockito.MockitoSugar
import org.scalatest.{MustMatchers, WordSpec}
import play.api.libs.json.Json
import play.api.libs.ws.{WSClient, WSRequest, WSResponse}
import scala.concurrent.Future
class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures with MockitoSugar {
val ws = mock[WSClient]
val ec = Executors.externalServiceEC
val config = new RPCConfig {
override def password: RPCConfig.Password = RPCConfig.Password("pass")
override def host: RPCConfig.Host = RPCConfig.Host("localhost")
override def username: RPCConfig.Username = RPCConfig.Username("user")
}
val request = mock[WSRequest]
val response = mock[WSResponse]
when(ws.url(anyString)).thenReturn(request)
when(request.withAuth(anyString(), anyString(), any())).thenReturn(request)
when(request.withHttpHeaders(any())).thenReturn(request)
val service = new XSNServiceRPCImpl(ws, config)(ec)
val txid = TransactionId.from("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c").get
"getTransaction" should {
"handle a successful result" in {
val responseBody =
"""
|{
| "result": {
| "blockhash": "000003fb382f6892ae96594b81aa916a8923c70701de4e7054aac556c7271ef7",
| "blocktime": 1520276270,
| "confirmations": 5347,
| "height": 1,
| "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff010000000000000000232103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac00000000",
| "locktime": 0,
| "size": 98,
| "time": 1520276270,
| "txid": "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c",
| "version": 1,
| "vin": [
| {
| "coinbase": "510101",
| "sequence": 4294967295
| }
| ],
| "vout": [
| {
| "n": 0,
| "scriptPubKey": {
| "addresses": [
| "XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"
| ],
| "asm": "03e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029 OP_CHECKSIG",
| "hex": "2103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac",
| "reqSigs": 1,
| "type": "pubkey"
| },
| "value": 0,
| "valueSat": 0
| }
| ]
| },
| "error": null,
| "id": null
|}
""".stripMargin.trim
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.getTransaction(txid)) { result =>
result.isGood mustEqual true
}
}
"handle transaction not found" in {
val responseBody = """{"result":null,"error":{"code":-5,"message":"No information available about transaction"},"id":null}"""
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.getTransaction(txid)) { result =>
result mustEqual Bad(TransactionNotFoundError).accumulating
}
}
"handle error with message" in {
val responseBody = """{"result":null,"error":{"code":-32600,"message":"Params must be an array"},"id":null}"""
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.getTransaction(txid)) { result =>
val error = XSNMessageError("Params must be an array")
result mustEqual Bad(error).accumulating
}
}
"handle unexpected error" in {
val responseBody = """{"result":null,"error":{},"id":null}"""
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.getTransaction(txid)) { result =>
result mustEqual Bad(XSNUnexpectedResponseError).accumulating
}
}
}
}
Loading…
Cancel
Save