diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index f0fcc2f..7f15d56 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -20,6 +20,8 @@ trait XSNService { def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] + + def getTransactionCount(address: Address): FutureApplicationResult[Int] } class XSNServiceRPCImpl @Inject() ( @@ -78,6 +80,33 @@ class XSNServiceRPCImpl @Inject() ( } } + override def getTransactionCount(address: Address): FutureApplicationResult[Int] = { + 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[JsValue]](response, errorCodeMapper) + maybe.map(_.map(_.size)).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] @@ -101,7 +130,7 @@ class XSNServiceRPCImpl @Inject() ( private def getResult[A]( response: WSResponse, - errorCodeMapper: Map[Int, ApplicationError] = Map.empty)( + errorCodeMapper: Map[Int, ApplicationError])( implicit reads: Reads[A]): Option[ApplicationResult[A]] = { Option(response) diff --git a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala new file mode 100644 index 0000000..f249104 --- /dev/null +++ b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala @@ -0,0 +1,12 @@ +package com.xsn.explorer.helpers + +import com.alexitc.playsonify.core.FutureApplicationResult +import com.xsn.explorer.models.{Address, AddressBalance, Transaction, TransactionId} +import com.xsn.explorer.services.XSNService + +class DummyXSNService extends XSNService { + + override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = ??? + override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = ??? + override def getTransactionCount(address: Address): FutureApplicationResult[Int] = ??? +} diff --git a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala index a62e86e..e083a9a 100644 --- a/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala +++ b/server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala @@ -6,7 +6,7 @@ import com.xsn.explorer.helpers.Executors import com.xsn.explorer.models.{Address, TransactionId} import org.mockito.ArgumentMatchers._ import org.mockito.Mockito._ -import org.scalactic.Bad +import org.scalactic.{Bad, Good} import org.scalatest.concurrent.ScalaFutures import org.scalatest.mockito.MockitoSugar import org.scalatest.{MustMatchers, OptionValues, WordSpec} @@ -278,4 +278,55 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures } } } + + "getTransactionCount" should { + "return the number of transactions" in { + val responseBody = + """ + |{ + | "result": [ + | "3963203e8ff99c0effbc7c90ef1b534f7e60d9d4d1d131375bc73eb6af8b62d0", + | "56eff1fc3ec29277a039944a10826e1bd24685bec5e5c46c946846cb859dc14b", + | "d5718b83b6cec30e075c20dc61005a25a6eb707f14b92e89c2a9c4bc39635b5d", + | "9cd1d22e786b7b8722ce51f551c3c5af2053a52bd7694b9ef79e0a5d95053b19", + | "1dbf0277891ed39f8175fa08844eadbb6ed4b28464fbac0ba88464001192d79e", + | "2520ec229a76db8efa8e3b384582ac5c1969224a97f32475b957151f0c8cdfa7", + | "46d2f51afeab7aeb7adab821c374c7348ae0ff4edb7d0c9af995360630194cc8", + | "1a91406280e2a77dc0baf8a13491a977cba2d2dae6a8ba93fc6bbd3a7aeec4e5", + | "def0ae8bbfa45dca177f9c9f169e362bd25dee460a8ddc8c662e92e6968cd6d8" + | ], + | "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.getTransactionCount(address)) { result => + result mustEqual Good(9) + } + } + + "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.getTransactionCount(address)) { result => + result mustEqual Bad(AddressFormatError).accumulating + } + } + } } diff --git a/server/test/controllers/TransactionsControllerSpec.scala b/server/test/controllers/TransactionsControllerSpec.scala index 5aa008b..1b5c292 100644 --- a/server/test/controllers/TransactionsControllerSpec.scala +++ b/server/test/controllers/TransactionsControllerSpec.scala @@ -3,6 +3,7 @@ package controllers import com.alexitc.playsonify.PublicErrorRenderer import com.alexitc.playsonify.core.FutureApplicationResult import com.xsn.explorer.errors.TransactionNotFoundError +import com.xsn.explorer.helpers.DummyXSNService import com.xsn.explorer.models._ import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec @@ -55,7 +56,7 @@ class TransactionsControllerSpec extends MyAPISpec { TransactionVOUT(n = 2, value = BigDecimal("2343749.96562500"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL"))) ) - val customXSNService = new XSNService { + val customXSNService = new DummyXSNService { val map = Map( "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c" -> coinbaseTx, "0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641" -> nonCoinbaseTx, @@ -71,8 +72,6 @@ class TransactionsControllerSpec extends MyAPISpec { Future.successful(result) } - - override def getAddressBalance(address: Address): FutureApplicationResult[AddressBalance] = ??? } override val application = guiceApplicationBuilder