Browse Source

server: Refactor the TransactionVIN into a typed alternative

Now we are able to use types to detect when a TransactionVIN has the
cached values.
master
Alexis Hernandez 6 years ago
parent
commit
ff85797565
  1. 12
      server/app/com/xsn/explorer/models/TransactionDetails.scala
  2. 16
      server/app/com/xsn/explorer/models/persisted/Transaction.scala
  3. 10
      server/app/com/xsn/explorer/models/rpc/Transaction.scala
  4. 29
      server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala
  5. 9
      server/app/com/xsn/explorer/services/BlockService.scala
  6. 24
      server/app/com/xsn/explorer/services/TransactionRPCService.scala
  7. 6
      server/app/com/xsn/explorer/services/XSNService.scala
  8. 8
      server/app/com/xsn/explorer/services/logic/BlockLogic.scala
  9. 6
      server/app/com/xsn/explorer/services/logic/TransactionLogic.scala
  10. 4
      server/test/com/xsn/explorer/helpers/DummyXSNService.scala
  11. 4
      server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala
  12. 8
      server/test/com/xsn/explorer/helpers/TransactionLoader.scala
  13. 4
      server/test/controllers/BlocksControllerSpec.scala
  14. 16
      server/test/controllers/TransactionsControllerSpec.scala

12
server/app/com/xsn/explorer/models/TransactionDetails.scala

@ -22,16 +22,14 @@ case class TransactionDetails(
object TransactionDetails {
def from(tx: rpc.Transaction, input: List[TransactionValue]): TransactionDetails = {
TransactionDetails
.from(tx)
.copy(input = input)
}
def from(tx: rpc.Transaction[rpc.TransactionVIN.HasValues]): TransactionDetails = {
val input = tx.vin.map { vin =>
TransactionValue(vin.address, vin.value)
}
def from(tx: rpc.Transaction): TransactionDetails = {
val output = tx.vout.flatMap(TransactionValue.from)
TransactionDetails(tx.id, tx.size, tx.blockhash, tx.time, tx.blocktime, tx.confirmations, List.empty, output)
TransactionDetails(tx.id, tx.size, tx.blockhash, tx.time, tx.blocktime, tx.confirmations, input, output)
}
implicit val writes: Writes[TransactionDetails] = Json.writes[TransactionDetails]

16
server/app/com/xsn/explorer/models/persisted/Transaction.scala

@ -1,6 +1,7 @@
package com.xsn.explorer.models.persisted
import com.xsn.explorer.models.rpc
import com.xsn.explorer.models.rpc.TransactionVIN
import com.xsn.explorer.models.values._
case class Transaction(
@ -52,13 +53,14 @@ object Transaction {
* If the rpc transaction might not be complete, get the input value and address using
* the utxo index or the getTransaction method from the TransactionService.
*/
def fromRPC(tx: rpc.Transaction): HasIO = {
val inputs = tx.vin.zipWithIndex.flatMap { case (vin, index) =>
for {
value <- vin.value
address <- vin.address
} yield Transaction.Input(vin.txid, vin.voutIndex, index, value, address)
}
def fromRPC[VIN <: TransactionVIN](tx: rpc.Transaction[VIN]): HasIO = {
val inputs = tx
.vin
.zipWithIndex
.collect { case (vin: rpc.TransactionVIN.HasValues, index) => (vin, index) }
.map { case (vin, index) =>
Transaction.Input(vin.txid, vin.voutIndex, index, vin.value, vin.address)
}
val outputs = tx.vout.flatMap { vout =>
val tposAddresses = vout.scriptPubKey.flatMap(_.getTPoSAddresses)

10
server/app/com/xsn/explorer/models/rpc/Transaction.scala

@ -1,24 +1,22 @@
package com.xsn.explorer.models.rpc
import com.xsn.explorer.models._
import com.xsn.explorer.models.values.{Blockhash, Confirmations, Size, TransactionId}
import play.api.libs.functional.syntax._
import play.api.libs.json._
case class Transaction(
case class Transaction[VIN <: TransactionVIN](
id: TransactionId,
size: Size,
blockhash: Blockhash,
time: Long,
blocktime: Long,
confirmations: Confirmations,
vin: List[TransactionVIN],
vout: List[TransactionVOUT],
)
vin: List[VIN],
vout: List[TransactionVOUT])
object Transaction {
implicit val reads: Reads[Transaction] = {
implicit val reads: Reads[Transaction[TransactionVIN]] = {
val builder = (__ \ 'txid).read[TransactionId] and
(__ \ 'size).read[Size] and
(__ \ 'blockhash).read[Blockhash] and

29
server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala

@ -4,22 +4,37 @@ import com.xsn.explorer.models.values.{Address, TransactionId}
import play.api.libs.functional.syntax._
import play.api.libs.json.{Reads, __}
case class TransactionVIN(
txid: TransactionId,
voutIndex: Int,
value: Option[BigDecimal],
address: Option[Address])
sealed trait TransactionVIN {
def txid: TransactionId
def voutIndex: Int
def withValues(value: BigDecimal, address: Address): TransactionVIN.HasValues = {
TransactionVIN.HasValues(txid, voutIndex, value, address)
}
}
object TransactionVIN {
final case class Raw(override val txid: TransactionId, override val voutIndex: Int) extends TransactionVIN
final case class HasValues(
override val txid: TransactionId,
override val voutIndex: Int,
value: BigDecimal,
address: Address) extends TransactionVIN
implicit val reads: Reads[TransactionVIN] = {
val builder = (__ \ 'txid).read[TransactionId] and
(__ \ 'vout).read[Int] and
(__ \ 'value).readNullable[BigDecimal] and
(__ \ 'address).readNullable[Address]
builder.apply { (txid, index, value, address) =>
TransactionVIN(txid, index, value, address)
builder.apply { (txid, index, valueMaybe, addressMaybe) =>
val maybe = for {
value <- valueMaybe
address <- addressMaybe
} yield HasValues(txid, index, value, address)
maybe.getOrElse(Raw(txid, index))
}
}
}

9
server/app/com/xsn/explorer/services/BlockService.scala

@ -255,10 +255,9 @@ class BlockService @Inject() (
result.toFuture
}
coinstakeTxVIN
.value
.map(Good(_))
.map(Future.successful)
.getOrElse(loadFromTx)
coinstakeTxVIN match {
case TransactionVIN.HasValues(_, _, value, _) => Future.successful(Good(value))
case _ => loadFromTx
}
}
}

24
server/app/com/xsn/explorer/services/TransactionRPCService.scala

@ -11,7 +11,7 @@ import com.xsn.explorer.util.Extensions.FutureOrExt
import javax.inject.Inject
import org.scalactic.{Bad, Good, One, Or}
import org.slf4j.LoggerFactory
import play.api.libs.json.{JsObject, JsString, JsValue, Json}
import play.api.libs.json.{JsString, JsValue, Json}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
@ -43,12 +43,8 @@ class TransactionRPCService @Inject() (
}
transaction <- xsnService.getTransaction(txid).toFutureOr
input <- transaction
.vin
.map(getTransactionValue)
.toFutureOr
} yield TransactionDetails.from(transaction, input)
vin <- getTransactionVIN(transaction.vin).toFutureOr
} yield TransactionDetails.from(transaction.copy(vin = vin))
result.toFuture
}
@ -63,19 +59,19 @@ class TransactionRPCService @Inject() (
result.toFuture
}
private def getTransactionVIN(list: List[TransactionVIN]): FutureApplicationResult[List[TransactionVIN]] = {
private def getTransactionVIN(list: List[TransactionVIN]): FutureApplicationResult[List[TransactionVIN.HasValues]] = {
def getVIN(vin: TransactionVIN) = {
getTransactionValue(vin)
.map {
case Good(transactionValue) =>
val newVIN = vin.copy(address = Some(transactionValue.address), value = Some(transactionValue.value))
val newVIN = vin.withValues(address = transactionValue.address, value = transactionValue.value)
Good(newVIN)
case Bad(e) => Bad(e)
}
}
def loadVINSequentially(pending: List[TransactionVIN]): FutureOr[List[TransactionVIN]] = pending match {
def loadVINSequentially(pending: List[TransactionVIN]): FutureOr[List[TransactionVIN.HasValues]] = pending match {
case x :: xs =>
for {
tx <- getVIN(x).toFutureOr
@ -132,10 +128,10 @@ class TransactionRPCService @Inject() (
}
private def getTransactionValue(vin: TransactionVIN): FutureApplicationResult[TransactionValue] = {
val valueMaybe = for {
value <- vin.value
address <- vin.address
} yield TransactionValue(address, value)
val valueMaybe = vin match {
case x: TransactionVIN.HasValues => Some(TransactionValue(x.address, x.value))
case _ => None
}
valueMaybe
.map(Good(_))

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

@ -18,7 +18,7 @@ import scala.util.Try
trait XSNService {
def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction]
def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction[rpc.TransactionVIN]]
def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue]
@ -73,14 +73,14 @@ class XSNServiceRPCImpl @Inject() (
.withHttpHeaders("Content-Type" -> "text/plain")
override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction] = {
override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction[rpc.TransactionVIN]] = {
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)
val maybe = getResult[rpc.Transaction[rpc.TransactionVIN]](response, errorCodeMapper)
maybe.getOrElse {
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}")

8
server/app/com/xsn/explorer/services/logic/BlockLogic.scala

@ -45,7 +45,7 @@ class BlockLogic {
Or.from(maybe, One(BlockNotFoundError))
}
def getTPoSAddresses(tposContract: Transaction): ApplicationResult[(Address, Address)] = {
def getTPoSAddresses(tposContract: Transaction[_]): ApplicationResult[(Address, Address)] = {
val maybe = tposContract
.vout
.flatMap(_.scriptPubKey)
@ -74,7 +74,7 @@ class BlockLogic {
* we return 0 in that case because the reward could be negative.
*/
def getPoSRewards(
coinstakeTx: Transaction,
coinstakeTx: Transaction[_],
coinstakeAddress: Address,
coinstakeInput: BigDecimal): ApplicationResult[PoSBlockRewards] = {
@ -106,7 +106,7 @@ class BlockLogic {
}
def getTPoSRewards(
coinstakeTx: Transaction,
coinstakeTx: Transaction[_],
owner: Address,
merchant: Address,
coinstakeInput: BigDecimal): ApplicationResult[TPoSBlockRewards] = {
@ -151,7 +151,7 @@ class BlockLogic {
Good(TPoSBlockRewards(ownerReward, merchantReward, masternodeRewardMaybe))
}
def isPoS(block: rpc.Block, coinbase: rpc.Transaction): Boolean = {
def isPoS(block: rpc.Block, coinbase: rpc.Transaction[_]): Boolean = {
block.nonce == 0 &&
coinbase.vin.isEmpty &&
coinbase.vout.flatMap(_.address).isEmpty

6
server/app/com/xsn/explorer/services/logic/TransactionLogic.scala

@ -13,16 +13,16 @@ class TransactionLogic {
Or.from(maybe, One(error))
}
def getVIN(tx: Transaction, error: ApplicationError): ApplicationResult[TransactionVIN] = {
def getVIN(tx: Transaction[TransactionVIN], error: ApplicationError): ApplicationResult[TransactionVIN] = {
val maybe = tx.vin.headOption
Or.from(maybe, One(error))
}
def getVOUT(vin: TransactionVIN, previousTX: Transaction, error: ApplicationError): ApplicationResult[TransactionVOUT] = {
def getVOUT(vin: TransactionVIN, previousTX: Transaction[_], error: ApplicationError): ApplicationResult[TransactionVOUT] = {
getVOUT(vin.voutIndex, previousTX, error)
}
def getVOUT(index: Int, previousTX: Transaction, error: ApplicationError): ApplicationResult[TransactionVOUT] = {
def getVOUT(index: Int, previousTX: Transaction[_], error: ApplicationError): ApplicationResult[TransactionVOUT] = {
val maybe = previousTX.vout.find(_.n == index)
Or.from(maybe, One(error))
}

4
server/test/com/xsn/explorer/helpers/DummyXSNService.scala

@ -2,7 +2,7 @@ package com.xsn.explorer.helpers
import com.alexitc.playsonify.core.FutureApplicationResult
import com.xsn.explorer.models._
import com.xsn.explorer.models.rpc.Masternode
import com.xsn.explorer.models.rpc.{Masternode, Transaction, TransactionVIN}
import com.xsn.explorer.models.values._
import com.xsn.explorer.services.XSNService
import play.api.libs.json.JsValue
@ -10,7 +10,7 @@ import play.api.libs.json.JsValue
class DummyXSNService extends XSNService {
override def genesisBlockhash: Blockhash = Blockhash.from("00000c822abdbb23e28f79a49d29b41429737c6c7e15df40d1b1f1b35907ae34").get
override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction] = ???
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction[TransactionVIN]] = ???
override def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = ???
override def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] = ???
override def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] = ???

4
server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala

@ -2,7 +2,7 @@ package com.xsn.explorer.helpers
import com.alexitc.playsonify.core.FutureApplicationResult
import com.xsn.explorer.errors.{BlockNotFoundError, TransactionNotFoundError}
import com.xsn.explorer.models.rpc.{Block, Transaction}
import com.xsn.explorer.models.rpc.{Block, Transaction, TransactionVIN}
import com.xsn.explorer.models.values.{Blockhash, Height, TransactionId}
import org.scalactic.{Good, One, Or}
import play.api.libs.json.JsValue
@ -39,7 +39,7 @@ class FileBasedXSNService extends DummyXSNService {
Future.successful(Good(block))
}
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = {
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction[TransactionVIN]] = {
val maybe = transactionMap.get(txid)
val result = Or.from(maybe, One(TransactionNotFoundError))
Future.successful(result)

8
server/test/com/xsn/explorer/helpers/TransactionLoader.scala

@ -2,15 +2,15 @@ package com.xsn.explorer.helpers
import java.io.File
import com.xsn.explorer.models.rpc.Transaction
import com.xsn.explorer.models.rpc.{Transaction, TransactionVIN}
import play.api.libs.json.{JsValue, Json}
object TransactionLoader {
private val BasePath = "transactions"
def get(txid: String): Transaction = {
json(txid).as[Transaction]
def get(txid: String): Transaction[TransactionVIN] = {
json(txid).as[Transaction[TransactionVIN]]
}
def json(txid: String): JsValue = {
@ -23,7 +23,7 @@ object TransactionLoader {
}
}
def all(): List[Transaction] = {
def all(): List[Transaction[TransactionVIN]] = {
val uri = getClass.getResource(s"/$BasePath")
new File(uri.getPath)
.listFiles()

4
server/test/controllers/BlocksControllerSpec.scala

@ -8,7 +8,7 @@ import com.xsn.explorer.data.TransactionBlockingDataHandler
import com.xsn.explorer.helpers._
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
import com.xsn.explorer.models.rpc.Block
import com.xsn.explorer.models.rpc.{Block, TransactionVIN}
import com.xsn.explorer.models.values.{Blockhash, Confirmations, Height, Size}
import com.xsn.explorer.services.XSNService
import controllers.common.MyAPISpec
@ -38,7 +38,7 @@ class BlocksControllerSpec extends MyAPISpec {
blockhash = blockhash,
time = tx.time,
size = tx.size,
sent = tx.vin.flatMap(_.value).sum,
sent = tx.vin.collect { case x: TransactionVIN.HasValues => x }.map(_.value).sum,
received = tx.vout.map(_.value).sum
)
}

16
server/test/controllers/TransactionsControllerSpec.scala

@ -6,7 +6,7 @@ import com.xsn.explorer.data.TransactionBlockingDataHandler
import com.xsn.explorer.errors.TransactionNotFoundError
import com.xsn.explorer.helpers.{DataHelper, DummyXSNService, TransactionDummyDataHandler, TransactionLoader}
import com.xsn.explorer.models._
import com.xsn.explorer.models.rpc.Transaction
import com.xsn.explorer.models.rpc.{Transaction, TransactionVIN}
import com.xsn.explorer.models.values.{Confirmations, Size, TransactionId}
import com.xsn.explorer.services.XSNService
import controllers.common.MyAPISpec
@ -38,7 +38,7 @@ class TransactionsControllerSpec extends MyAPISpec {
severalInputsTx.id -> severalInputsTx
)
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = {
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction[TransactionVIN]] = {
val result = map.get(txid)
.map(Good(_))
.getOrElse {
@ -92,13 +92,17 @@ class TransactionsControllerSpec extends MyAPISpec {
}
"return non-coinbase transaction" in {
val tx = nonCoinbaseTx
val input = List(
TransactionValue(
createAddress("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL"),
BigDecimal("2343749.965625"))
)
val details = TransactionDetails.from(nonCoinbaseTx, input)
BigDecimal("2343749.965625")))
.map { v =>
TransactionVIN.HasValues(createTransactionId("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c"), 0, v.value, v.address)
}
val tx = nonCoinbaseTx.copy(vin = input)
val details = TransactionDetails.from(tx)
val response = GET(url(tx.id.string))
status(response) mustEqual OK

Loading…
Cancel
Save