package com.xsn.explorer.services import javax.inject.Inject import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} import com.alexitc.playsonify.models.ApplicationError import com.xsn.explorer.config.RPCConfig import com.xsn.explorer.errors._ import com.xsn.explorer.executors.ExternalServiceExecutionContext import com.xsn.explorer.models._ import org.scalactic.{Bad, Good} import org.slf4j.LoggerFactory 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[rpc.Transaction] def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] def getLatestBlock(): FutureApplicationResult[rpc.Block] def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] def getMasternodeCount(): FutureApplicationResult[Int] def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] } 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") 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[rpc.Transaction](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}") Bad(XSNUnexpectedResponseError).accumulating } } } override def getAddressBalance(address: Address): FutureApplicationResult[rpc.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[rpc.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 } } } override def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] = { val body = s""" |{ | "jsonrpc": "1.0", | "method": "getaddresstxids", | "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[List[TransactionId]](response, errorCodeMapper) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, address = ${address.string}, response = ${response.body}") Bad(XSNUnexpectedResponseError).accumulating } } } override def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] = { val errorCodeMapper = Map(-5 -> BlockNotFoundError) val body = s"""{ "jsonrpc": "1.0", "method": "getblock", "params": ["${blockhash.string}"] }""" server .post(body) .map { response => 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}") Bad(XSNUnexpectedResponseError).accumulating } } } override def getLatestBlock(): FutureApplicationResult[rpc.Block] = { val body = s""" |{ | "jsonrpc": "1.0", | "method": "getbestblockhash", | "params": [] |} |""".stripMargin server .post(body) .flatMap { response => val result = for { blockhash <- getResult[Blockhash](response) .orElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}") None } .getOrElse(Bad(XSNUnexpectedResponseError).accumulating) .toFutureOr block <- getBlock(blockhash).toFutureOr } yield block result.toFuture } } override def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] = { val body = s""" |{ | "jsonrpc": "1.0", | "method": "gettxoutsetinfo", | "params": [] |} |""".stripMargin server .post(body) .map { response => val maybe = getResult[rpc.ServerStatistics](response) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}") Bad(XSNUnexpectedResponseError).accumulating } } } override def getMasternodeCount(): FutureApplicationResult[Int] = { val body = s""" |{ | "jsonrpc": "1.0", | "method": "masternode", | "params": ["count"] |} |""".stripMargin server .post(body) .map { response => val maybe = getResult[Int](response) maybe.getOrElse { logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}") Bad(XSNUnexpectedResponseError).accumulating } } } 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] .filter(_ != JsNull) jsonErrorMaybe .flatMap { jsonError => // from error code if possible (jsonError \ "code") .asOpt[Int] .flatMap(errorCodeMapper.get) .orElse { // from message (jsonError \ "message") .asOpt[String] .filter(_.nonEmpty) .map(XSNMessageError.apply) } } } private def getResult[A]( response: WSResponse, errorCodeMapper: Map[Int, ApplicationError] = Map.empty)( implicit reads: Reads[A]): Option[ApplicationResult[A]] = { val maybe = 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) } } // if there is no result nor error, it is probably that the server returned non 200 status maybe.orElse { Try(response.json) .toOption .flatMap { json => mapError(json, errorCodeMapper) } .map { e => Bad(e).accumulating } } } }