From 5a7fd8a81d872cddd70a92d3409e2f8a93a5b397 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sun, 17 Jun 2018 19:18:38 -0500 Subject: [PATCH] server: Add endpoint "GET /addresses/:address/utxos" This is a part for #23, it allows to retrieve the unspent outputs for the given address. --- .../explorer/services/AddressService.scala | 23 ++++++--- .../app/controllers/AddressesController.scala | 4 ++ server/conf/routes | 1 + .../controllers/AddressesControllerSpec.scala | 47 ++++++++++++++++++- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/server/app/com/xsn/explorer/services/AddressService.scala b/server/app/com/xsn/explorer/services/AddressService.scala index 65d5e93..0dc8dd8 100644 --- a/server/app/com/xsn/explorer/services/AddressService.scala +++ b/server/app/com/xsn/explorer/services/AddressService.scala @@ -2,11 +2,12 @@ package com.xsn.explorer.services import javax.inject.Inject -import com.alexitc.playsonify.core.FutureApplicationResult import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} +import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} import com.xsn.explorer.errors.AddressFormatError import com.xsn.explorer.models.{Address, AddressDetails} import org.scalactic.{One, Or} +import play.api.libs.json.JsValue import scala.concurrent.ExecutionContext @@ -14,15 +15,25 @@ class AddressService @Inject() (xsnService: XSNService)(implicit ec: ExecutionCo def getDetails(addressString: String): FutureApplicationResult[AddressDetails] = { val result = for { - address <- { - val maybe = Address.from(addressString) - Or.from(maybe, One(AddressFormatError)).toFutureOr - } - + address <- getAddress(addressString).toFutureOr balance <- xsnService.getAddressBalance(address).toFutureOr transactions <- xsnService.getTransactions(address).toFutureOr } yield AddressDetails(balance, transactions) result.toFuture } + + def getUnspentOutputs(addressString: String): FutureApplicationResult[JsValue] = { + val result = for { + address <- getAddress(addressString).toFutureOr + outputs <- xsnService.getUnspentOutputs(address).toFutureOr + } yield outputs + + result.toFuture + } + + private def getAddress(addressString: String): ApplicationResult[Address] = { + val maybe = Address.from(addressString) + Or.from(maybe, One(AddressFormatError)) + } } diff --git a/server/app/controllers/AddressesController.scala b/server/app/controllers/AddressesController.scala index 6064339..4c0548c 100644 --- a/server/app/controllers/AddressesController.scala +++ b/server/app/controllers/AddressesController.scala @@ -21,4 +21,8 @@ class AddressesController @Inject() ( transactionService.getTransactions(address, paginatedQuery) } + + def getUnspentOutputs(address: String) = publicNoInput { _ => + addressService.getUnspentOutputs(address) + } } diff --git a/server/conf/routes b/server/conf/routes index f135dd1..81f88b5 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -10,6 +10,7 @@ GET /transactions/:txid/raw controllers.TransactionsController.getRawTransa GET /addresses/:address controllers.AddressesController.getDetails(address: String) GET /addresses/:address/transactions controllers.AddressesController.getTransactions(address: String, offset: Int ?= 0, limit: Int ?= 10) +GET /addresses/:address/utxos controllers.AddressesController.getUnspentOutputs(address: String) GET /blocks controllers.BlocksController.getLatestBlocks() GET /blocks/:query controllers.BlocksController.getDetails(query: String) diff --git a/server/test/controllers/AddressesControllerSpec.scala b/server/test/controllers/AddressesControllerSpec.scala index b825ca7..74fc1d7 100644 --- a/server/test/controllers/AddressesControllerSpec.scala +++ b/server/test/controllers/AddressesControllerSpec.scala @@ -8,9 +8,9 @@ import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.AddressBalance import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec -import org.scalactic.{One, Or} +import org.scalactic.{Good, One, Or} import play.api.inject.bind -import play.api.libs.json.JsValue +import play.api.libs.json.{JsValue, Json} import play.api.test.Helpers._ import scala.concurrent.Future @@ -29,6 +29,29 @@ class AddressesControllerSpec extends MyAPISpec { ) ) + val addressForUtxos = DataHelper.createAddress("XeNEPsgeWqNbrEGEN5vqv4wYcC3qQrqNyp") + val utxosResponse = + """ + |[ + | { + | "address": "XeNEPsgeWqNbrEGEN5vqv4wYcC3qQrqNyp", + | "height": 22451, + | "outputIndex": 0, + | "satoshis": 1500000000000, + | "script": "76a914285b6f1ccacea0059ff5393cb4eb2f0569e2b3e988ac", + | "txid": "ea837f2011974b6a1a2fa077dc33684932c514a4ec6febc10e1a19ebe1336539" + | }, + | { + | "address": "XeNEPsgeWqNbrEGEN5vqv4wYcC3qQrqNyp", + | "height": 25093, + | "outputIndex": 3, + | "satoshis": 2250000000, + | "script": "76a914285b6f1ccacea0059ff5393cb4eb2f0569e2b3e988ac", + | "txid": "96a06b802d1c15818a42aa9b46dd2e236cde746000d35f74d3eb940ab9d5694d" + | } + |] + """.stripMargin + val customXSNService = new DummyXSNService { val map = Map( "Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw" -> addressEmpty, @@ -46,6 +69,15 @@ class AddressesControllerSpec extends MyAPISpec { val result = Or.from(maybe, One(AddressFormatError)) Future.successful(result) } + + override def getUnspentOutputs(address: Address): FutureApplicationResult[JsValue] = { + if (address == addressForUtxos) { + val result = Good(Json.parse(utxosResponse)) + Future.successful(result) + } else { + super.getUnspentOutputs(address) + } + } } override val application = guiceApplicationBuilder @@ -82,4 +114,15 @@ class AddressesControllerSpec extends MyAPISpec { (error \ "field").as[String] mustEqual "address" } } + + "GET /addresses/:address/utxos" should { + def url(address: String) = s"/addresses/$address/utxos" + + "return an array with the result" in { + val response = GET(url(addressForUtxos.string)) + + status(response) mustEqual OK + contentAsJson(response) mustEqual Json.parse(utxosResponse) + } + } }