From 5313e7673847d5037ddf959952df8a1c36e8da51 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sun, 17 Jun 2018 20:37:41 -0500 Subject: [PATCH] server: Add sendRawTransaction method to XSNService This is a part for #26. --- .../explorer/errors/transactionErrors.scala | 20 ++++++- .../com/xsn/explorer/models/HexString.scala | 16 ++++++ .../xsn/explorer/services/XSNService.scala | 30 ++++++++++ server/conf/messages | 2 + .../explorer/helpers/DummyXSNService.scala | 1 + .../xsn/explorer/models/HexStringSpec.scala | 56 +++++++++++++++++++ 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 server/app/com/xsn/explorer/models/HexString.scala create mode 100644 server/test/com/xsn/explorer/models/HexStringSpec.scala diff --git a/server/app/com/xsn/explorer/errors/transactionErrors.scala b/server/app/com/xsn/explorer/errors/transactionErrors.scala index 0204375b..42a23642 100644 --- a/server/app/com/xsn/explorer/errors/transactionErrors.scala +++ b/server/app/com/xsn/explorer/errors/transactionErrors.scala @@ -1,6 +1,6 @@ package com.xsn.explorer.errors -import com.alexitc.playsonify.models.{FieldValidationError, InputValidationError, PublicError, ServerError} +import com.alexitc.playsonify.models._ import play.api.i18n.{Lang, MessagesApi} sealed trait TransactionError @@ -29,3 +29,21 @@ case object TransactionUnknownError extends TransactionError with ServerError { override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = List.empty } + +case object InvalidRawTransactionError extends TransactionError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.rawTransaction.invalid") + val error = FieldValidationError("hex", message) + List(error) + } +} + +case object RawTransactionAlreadyExistsError extends TransactionError with ConflictError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.rawTransaction.repeated") + val error = FieldValidationError("hex", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/models/HexString.scala b/server/app/com/xsn/explorer/models/HexString.scala new file mode 100644 index 00000000..d049c370 --- /dev/null +++ b/server/app/com/xsn/explorer/models/HexString.scala @@ -0,0 +1,16 @@ +package com.xsn.explorer.models + +class HexString private (val string: String) extends AnyVal + +object HexString { + + private val RegEx = "^[A-Fa-f0-9]+$" + + def from(string: String): Option[HexString] = { + if (string.length % 2 == 0 && string.matches(RegEx)) { + Option(new HexString(string)) + } else { + None + } + } +} diff --git a/server/app/com/xsn/explorer/services/XSNService.scala b/server/app/com/xsn/explorer/services/XSNService.scala index 93289803..b5c832d1 100644 --- a/server/app/com/xsn/explorer/services/XSNService.scala +++ b/server/app/com/xsn/explorer/services/XSNService.scala @@ -43,6 +43,8 @@ trait XSNService { def getMasternode(ipAddress: IPAddress): FutureApplicationResult[rpc.Masternode] def getUnspentOutputs(address: Address): FutureApplicationResult[JsValue] + + def sendRawTransaction(hex: HexString): FutureApplicationResult[Unit] } class XSNServiceRPCImpl @Inject() ( @@ -347,6 +349,34 @@ class XSNServiceRPCImpl @Inject() ( } } + override def sendRawTransaction(hex: HexString): FutureApplicationResult[Unit] = { + val errorCodeMapper = Map( + -26 -> InvalidRawTransactionError, + -22 -> InvalidRawTransactionError, + -27 -> RawTransactionAlreadyExistsError) + + val body = s""" + |{ + | "jsonrpc": "1.0", + | "method": "sendrawtransaction", + | "params": ["${hex.string}"] + |} + |""".stripMargin + + server + .post(body) + .map { response => + val maybe = getResult[String](response, errorCodeMapper) + .map { _.map(_ => ()) } + + 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] diff --git a/server/conf/messages b/server/conf/messages index 3254af40..faa6fd55 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -4,6 +4,8 @@ xsn.server.unexpectedError=Unexpected error from the XSN network error.transaction.format=Invalid transaction format error.transaction.notFound=Transaction not found +error.rawTransaction.invalid=The transaction is invalid +error.rawTransaction.repeated=The transaction is already in the network error.address.format=Invalid address format diff --git a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala index 73a4c3ab..f1b25ed9 100644 --- a/server/test/com/xsn/explorer/helpers/DummyXSNService.scala +++ b/server/test/com/xsn/explorer/helpers/DummyXSNService.scala @@ -21,4 +21,5 @@ class DummyXSNService extends XSNService { override def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] = ??? override def getMasternode(ipAddress: IPAddress): FutureApplicationResult[Masternode] = ??? override def getUnspentOutputs(address: Address): FutureApplicationResult[JsValue] = ??? + override def sendRawTransaction(hex: HexString): FutureApplicationResult[Unit] = ??? } diff --git a/server/test/com/xsn/explorer/models/HexStringSpec.scala b/server/test/com/xsn/explorer/models/HexStringSpec.scala new file mode 100644 index 00000000..3a38ad6d --- /dev/null +++ b/server/test/com/xsn/explorer/models/HexStringSpec.scala @@ -0,0 +1,56 @@ +package com.xsn.explorer.models + +import org.scalatest.{MustMatchers, OptionValues, WordSpec} + +class HexStringSpec extends WordSpec with MustMatchers with OptionValues { + + "from" should { + "accept a valid hex" in { + val string = "0100000001d036c70b1df769fa3205f8ff4e361af84073aa14c89de80488048b6ae4904ce9010000006a47304402201f1f9aef5d60f6e84714dfb98ca87ca8a146a2e04a3811d8f0aa770d8ac1c906022054e27a26f806a5d0c0e08332be186a96ee1ac951b8d1e6e3b10072d51eb6dd300121026648fd298f1cc06c474db0864720a9774efbe789dd67b2a46086f9754e4cc3f2ffffffff030000000000000000000e77b9932d0000001976a91436e0c51c93a357e23621bb993d28e5c18f95bb5588ac00d2496b000000001976a9149d1fef13c02f2f23cf9a09ba11987e90Dbf5910d88ac00000000" + val result = HexString.from(string) + result.value.string mustEqual string + } + + "accept a valid hex with mixed case" in { + val string = "0100000001d036c70b1df769fA3205f8fF4e361af84073aa14c89de80488048b6aE4904ce9010000006a47304402201f1f9aef5d60f6e84714dfb98ca87ca8a146a2e04a3811d8f0aa770d8ac1c906022054e27a26f806a5d0c0e08332be186a96ee1ac951b8d1e6e3b10072d51eb6dd300121026648fd298f1cc06c474db0864720a9774efbe789dd67b2a46086f9754e4cc3f2ffffffff030000000000000000000e77b9932d0000001976a91436e0c51c93a357e23621bb993d28e5c18f95bb5588ac00d2496b000000001976a9149d1fef13c02f2f23cf9a09ba11987e90Dbf5910d88ac00000000" + val result = HexString.from(string) + result.value.string mustEqual string + } + + "accept a string with all hex characters" in { + val string = "abcdef0123456789ABCDEF" + val result = HexString.from(string) + result.value.string mustEqual string + } + + "accept a two characters string" in { + val string = "0f" + val result = HexString.from(string) + result.value.string mustEqual string + } + + "reject an empty string" in { + val string = "" + val result = HexString.from(string) + result.isEmpty mustEqual true + } + + "reject a single character" in { + val string = "a" + val result = HexString.from(string) + result.isEmpty mustEqual true + } + + "reject spaces" in { + val string = "a " + val result = HexString.from(string) + result.isEmpty mustEqual true + } + + "reject non-hex characters" in { + val string = "abcdefg" + val result = HexString.from(string) + result.isEmpty mustEqual true + } + } +}