Browse Source

server: Update XSNService to return a Transaction

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
5fe1fe63ec
  1. 23
      server/app/com/xsn/explorer/services/TransactionService.scala
  2. 17
      server/app/com/xsn/explorer/services/XSNService.scala
  3. 110
      server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala
  4. 142
      server/test/controllers/TransactionsControllerSpec.scala

23
server/app/com/xsn/explorer/services/TransactionService.scala

@ -5,15 +5,14 @@ import javax.inject.Inject
import com.alexitc.playsonify.core.FutureApplicationResult
import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps}
import com.xsn.explorer.errors.TransactionFormatError
import com.xsn.explorer.models.TransactionId
import org.scalactic.{One, Or}
import play.api.libs.json.JsValue
import com.xsn.explorer.models.{TransactionDetails, TransactionId}
import org.scalactic.{Good, One, Or}
import scala.concurrent.ExecutionContext
import scala.concurrent.{ExecutionContext, Future}
class TransactionService @Inject() (xsnService: XSNService)(implicit ec: ExecutionContext) {
def getTransaction(txidString: String): FutureApplicationResult[JsValue] = {
def getTransaction(txidString: String): FutureApplicationResult[TransactionDetails] = {
val result = for {
txid <- {
val maybe = TransactionId.from(txidString)
@ -21,7 +20,19 @@ class TransactionService @Inject() (xsnService: XSNService)(implicit ec: Executi
}
transaction <- xsnService.getTransaction(txid).toFutureOr
} yield transaction
previousMaybe <- transaction
.vin
.map(_.txid)
.map(xsnService.getTransaction)
.map { f => f.toFutureOr.map(Option.apply).toFuture }
.getOrElse { Future.successful(Good(Option.empty))}
.toFutureOr
} yield {
previousMaybe
.map { previous => TransactionDetails.from(transaction, previous) }
.getOrElse { TransactionDetails.from(transaction) }
}
result.toFuture
}

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

@ -7,7 +7,7 @@ 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 com.xsn.explorer.models.{Transaction, TransactionId}
import org.scalactic.{Bad, Good}
import org.slf4j.LoggerFactory
import play.api.libs.json.{JsNull, JsValue}
@ -17,7 +17,7 @@ import scala.util.Try
trait XSNService {
def getTransaction(txid: TransactionId): FutureApplicationResult[JsValue]
def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction]
}
class XSNServiceRPCImpl @Inject() (
@ -32,18 +32,17 @@ class XSNServiceRPCImpl @Inject() (
.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] = {
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = {
server
.post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""")
.map { response =>
val maybe = Try(response.json)
.toOption
val maybe = Option(response)
.filter(_.status == 200)
.flatMap { r => Try(r.json).toOption }
.flatMap { json =>
(json \ "result")
.asOpt[JsValue]
.filter(_ != JsNull)
.asOpt[Transaction]
.map { Good(_) }
.orElse {
mapError(json)
@ -53,7 +52,7 @@ class XSNServiceRPCImpl @Inject() (
}
maybe.getOrElse {
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, response = ${response.body}")
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}")
Bad(XSNUnexpectedResponseError).accumulating
}

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

@ -9,13 +9,13 @@ 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 org.scalatest.{MustMatchers, OptionValues, WordSpec}
import play.api.libs.json.{JsNull, Json}
import play.api.libs.ws.{WSClient, WSRequest, WSResponse}
import scala.concurrent.Future
class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures with MockitoSugar {
class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures with MockitoSugar with OptionValues {
val ws = mock[WSClient]
val ec = Executors.externalServiceEC
@ -35,7 +35,7 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures
val txid = TransactionId.from("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c").get
"getTransaction" should {
"handle a successful result" in {
"handle coinbase" in {
val responseBody =
"""
|{
@ -86,6 +86,98 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures
whenReady(service.getTransaction(txid)) { result =>
result.isGood mustEqual true
val tx = result.get
tx.id.string mustEqual "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c"
tx.vin.isEmpty mustEqual true
tx.vout.size mustEqual 1
}
}
"handle non-coinbase result" in {
val responseBody =
"""
|{
| "result": {
| "hex": "0100000001d967897603771672654db507a02ceb65dea8a682d2333ee819cac80950ec5c58020000006a473044022059a0cc21ad24ae18726d128c85328a0b54dab62aeb41ffbcad368ece6fdf9d2602200e477332401ce1296d379dc5f797720e854e40fc5af0a268f585e7dae64d38e5012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff03000000000000000000220935d7946a00001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac220935d7946a00001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000",
| "txid": "0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641",
| "size": 234,
| "version": 1,
| "locktime": 0,
| "vin": [
| {
| "txid": "585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9",
| "vout": 2,
| "scriptSig": {
| "asm": "3044022059a0cc21ad24ae18726d128c85328a0b54dab62aeb41ffbcad368ece6fdf9d2602200e477332401ce1296d379dc5f797720e854e40fc5af0a268f585e7dae64d38e5[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1",
| "hex": "473044022059a0cc21ad24ae18726d128c85328a0b54dab62aeb41ffbcad368ece6fdf9d2602200e477332401ce1296d379dc5f797720e854e40fc5af0a268f585e7dae64d38e5012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1"
| },
| "sequence": 4294967295
| }
| ],
| "vout": [
| {
| "value": 0.00000000,
| "valueSat": 0,
| "n": 0,
| "scriptPubKey": {
| "asm": "",
| "hex": "",
| "type": "nonstandard"
| }
| },
| {
| "value": 1171874.98281250,
| "valueSat": 117187498281250,
| "n": 1,
| "scriptPubKey": {
| "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG",
| "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac",
| "reqSigs": 1,
| "type": "pubkeyhash",
| "addresses": [
| "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL"
| ]
| }
| },
| {
| "value": 1171874.98281250,
| "valueSat": 117187498281250,
| "n": 2,
| "scriptPubKey": {
| "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG",
| "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac",
| "reqSigs": 1,
| "type": "pubkeyhash",
| "addresses": [
| "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL"
| ]
| }
| }
| ],
| "blockhash": "b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81",
| "height": 809,
| "confirmations": 1950,
| "time": 1520318120,
| "blocktime": 1520318120
| },
| "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
val tx = result.get
tx.id.string mustEqual "0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641"
tx.vin.value.txid.string mustEqual "585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9"
tx.vout.size mustEqual 3
}
}
@ -115,6 +207,16 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures
}
}
"handle non successful status" in {
when(response.status).thenReturn(403)
when(response.json).thenReturn(JsNull)
when(request.post[String](anyString())(any())).thenReturn(Future.successful(response))
whenReady(service.getTransaction(txid)) { result =>
result mustEqual Bad(XSNUnexpectedResponseError).accumulating
}
}
"handle unexpected error" in {
val responseBody = """{"result":null,"error":{},"id":null}"""
val json = Json.parse(responseBody)

142
server/test/controllers/TransactionsControllerSpec.scala

@ -3,62 +3,67 @@ package controllers
import com.alexitc.playsonify.PublicErrorRenderer
import com.alexitc.playsonify.core.FutureApplicationResult
import com.xsn.explorer.errors.TransactionNotFoundError
import com.xsn.explorer.models.TransactionId
import com.xsn.explorer.models._
import com.xsn.explorer.services.XSNService
import controllers.common.MyAPISpec
import org.scalactic.{Bad, Good}
import play.api.inject.bind
import play.api.libs.json.{JsValue, Json}
import play.api.libs.json.JsValue
import play.api.test.Helpers._
import scala.concurrent.Future
class TransactionsControllerSpec extends MyAPISpec {
val coinbaseTx: Transaction = Transaction(
id = TransactionId.from("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c").get,
size = 98,
blockhash = Blockhash.from("000003fb382f6892ae96594b81aa916a8923c70701de4e7054aac556c7271ef7").get,
time = 1520276270L,
blocktime = 1520276270L,
confirmations = 5347,
vin = None,
vout = List(
TransactionVOUT(n = 0, address = Address.from("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"), value = 0, scriptPubKeyType = "pubkey"))
)
val nonCoinbaseTx: Transaction = Transaction(
id = TransactionId.from("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641").get,
size = 234,
blockhash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81").get,
time = 1520318120,
blocktime = 1520318120,
confirmations = 1950,
vin = Some(
TransactionVIN(TransactionId.from("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9").get, 2)),
vout = List(
TransactionVOUT(n = 1, value = BigDecimal("1171874.98281250"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")),
TransactionVOUT(n = 2, value = BigDecimal("1171874.98281250"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")))
)
val nonCoinbasePreviousTx: Transaction = Transaction(
id = TransactionId.from("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9").get,
size = 235,
blockhash = Blockhash.from("cc4ccf19cfb9fa373ba8da68c7d25266d675a2414db603edb3cc88f866a782ea").get,
time = 1520314409,
blocktime = 1520314409,
confirmations = 11239,
vin = Some(
TransactionVIN(TransactionId.from("fd74206866fc4ed986d39084eb9f20de6cb324b028693f33d60897ac995fff4f").get, 2)),
vout = List(
TransactionVOUT(n = 1, value = BigDecimal("2343749.96562500"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")),
TransactionVOUT(n = 2, value = BigDecimal("2343749.96562500"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")))
)
val customXSNService = new XSNService {
val map = Map(
TransactionId.from("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c").get ->
s"""
|{
| "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
| }
| ]
|}
""".stripMargin.trim
"024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c" -> coinbaseTx,
"0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641" -> nonCoinbaseTx,
"585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9" -> nonCoinbasePreviousTx
)
override def getTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = {
val result = map.get(txid)
.map(s => Json.toJson(s))
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = {
val result = map.get(txid.string)
.map(Good(_))
.getOrElse {
Bad(TransactionNotFoundError).accumulating
@ -75,14 +80,55 @@ class TransactionsControllerSpec extends MyAPISpec {
"GET /transactions/:txid" should {
def url(txid: String) = s"/transactions/$txid"
"return an existing transaction" in {
val txid = "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c"
val response = GET(url(txid))
"return coinbase transaction" in {
val tx = coinbaseTx
val response = GET(url(tx.id.string))
status(response) mustEqual OK
val json = contentAsJson(response)
(json \ "id").as[String] mustEqual tx.id.string
(json \ "blockhash").as[String] mustEqual tx.blockhash.string
(json \ "size").as[Int] mustEqual tx.size
(json \ "time").as[Long] mustEqual tx.time
(json \ "blocktime").as[Long] mustEqual tx.blocktime
(json \ "confirmations").as[Int] mustEqual tx.confirmations
val outputJsonList = (json \ "output").as[List[JsValue]]
outputJsonList.size mustEqual 1
val outputJson = outputJsonList.head
(outputJson \ "address").as[String] mustEqual tx.vout.head.address.get.string
(outputJson \ "value").as[BigDecimal] mustEqual tx.vout.head.value
}
"return non-coinbase transaction" in {
val tx = nonCoinbaseTx
val details = TransactionDetails.from(nonCoinbaseTx, nonCoinbasePreviousTx)
val response = GET(url(tx.id.string))
status(response) mustEqual OK
// TODO: Match result
//val json = contentAsJson(response)
//(json \ "txid").as[String] mustEqual txid
val json = contentAsJson(response)
(json \ "id").as[String] mustEqual tx.id.string
(json \ "blockhash").as[String] mustEqual tx.blockhash.string
(json \ "size").as[Int] mustEqual tx.size
(json \ "time").as[Long] mustEqual tx.time
(json \ "blocktime").as[Long] mustEqual tx.blocktime
(json \ "confirmations").as[Int] mustEqual tx.confirmations
val inputJson = (json \ "input").as[JsValue]
(inputJson \ "address").as[String] mustEqual details.input.get.address.string
(inputJson \ "value").as[BigDecimal] mustEqual details.input.get.value
val outputJsonList = (json \ "output").as[List[JsValue]]
outputJsonList.size mustEqual 2
val outputJson = outputJsonList.head
(outputJson \ "address").as[String] mustEqual details.output.head.address.string
(outputJson \ "value").as[BigDecimal] mustEqual details.output.head.value
val outputJson2 = outputJsonList.drop(1).head
(outputJson2 \ "address").as[String] mustEqual details.output.drop(1).head.address.string
(outputJson2 \ "value").as[BigDecimal] mustEqual details.output.drop(1).head.value
}
"fail on wrong transaction format" in {

Loading…
Cancel
Save