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