From 80f02664e528e3d64c423a0fc1223b1246b79565 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 30 Mar 2019 17:25:25 -0700 Subject: [PATCH] server: Update the persisted Transaction model - Allow mapping from rpc transaction with values only - While mapping from rpc transactions, return the TPoS contract if there is one. --- .../models/persisted/Transaction.scala | 35 +++++-- .../services/TransactionRPCService.scala | 2 +- .../TransactionPostgresDataHandlerSpec.scala | 3 +- .../xsn/explorer/helpers/LedgerHelper.scala | 3 +- .../explorer/helpers/TransactionLoader.scala | 16 ++- .../models/persisted/TransactionSpec.scala | 97 +++++++++++++++++++ 6 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 server/test/com/xsn/explorer/models/persisted/TransactionSpec.scala diff --git a/server/app/com/xsn/explorer/models/persisted/Transaction.scala b/server/app/com/xsn/explorer/models/persisted/Transaction.scala index 4f61b41..5de4002 100644 --- a/server/app/com/xsn/explorer/models/persisted/Transaction.scala +++ b/server/app/com/xsn/explorer/models/persisted/Transaction.scala @@ -1,6 +1,6 @@ package com.xsn.explorer.models.persisted -import com.xsn.explorer.models.rpc +import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.TransactionVIN import com.xsn.explorer.models.values._ @@ -46,26 +46,23 @@ object Transaction { } /** - * Please note that the inputs might not be accurate. + * Transform a rpc transaction to a persisted 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. + * As the TPoS contracts aren't stored in the persisted transaction, they are returned on the result. */ - def fromRPC[VIN <: TransactionVIN](tx: rpc.Transaction[VIN]): HasIO = { + def fromRPC(tx: rpc.Transaction[TransactionVIN.HasValues]): (HasIO, Option[TPoSContract]) = { 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 scriptMaybe = vout.scriptPubKey.map(_.hex) for { address <- vout.address - script <- scriptMaybe - } yield Transaction.Output( + script <- vout.scriptPubKey.map(_.hex) + } yield Output( tx.id, vout.n, vout.value, @@ -80,6 +77,24 @@ object Transaction { size = tx.size ) - HasIO(transaction, inputs, outputs) + (HasIO(transaction, inputs, outputs), getContract(tx)) + } + + /** + * A transaction can have at most one contract + */ + private def getContract(tx: rpc.Transaction[rpc.TransactionVIN.HasValues]): Option[TPoSContract] = { + val collateralMaybe = tx.vout.find(_.value == 1) + val detailsMaybe = tx.vout.flatMap(_.scriptPubKey).flatMap(_.getTPoSContractDetails).headOption + + for { + collateral <- collateralMaybe + details <- detailsMaybe + } yield TPoSContract( + TPoSContract.Id(tx.id, collateral.n), + time = tx.time, + details = details, + state = TPoSContract.State.Active + ) } } diff --git a/server/app/com/xsn/explorer/services/TransactionRPCService.scala b/server/app/com/xsn/explorer/services/TransactionRPCService.scala index dadefd2..8213264 100644 --- a/server/app/com/xsn/explorer/services/TransactionRPCService.scala +++ b/server/app/com/xsn/explorer/services/TransactionRPCService.scala @@ -54,7 +54,7 @@ class TransactionRPCService @Inject() ( tx <- xsnService.getTransaction(txid).toFutureOr transactionVIN <- getTransactionVIN(tx.vin).toFutureOr rpcTransaction = tx.copy(vin = transactionVIN) - } yield Transaction.fromRPC(rpcTransaction) + } yield Transaction.fromRPC(rpcTransaction)._1 result.toFuture } diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index 86b1c33..f708f7b 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala @@ -384,8 +384,9 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be private def createBlock(block: Block) = { val transactions = block.transactions .map(_.string) - .map(TransactionLoader.get) + .map(TransactionLoader.getWithValues) .map(Transaction.fromRPC) + .map(_._1) val result = ledgerDataHandler.push(block.withTransactions(transactions)) diff --git a/server/test/com/xsn/explorer/helpers/LedgerHelper.scala b/server/test/com/xsn/explorer/helpers/LedgerHelper.scala index 443f567..22d05eb 100644 --- a/server/test/com/xsn/explorer/helpers/LedgerHelper.scala +++ b/server/test/com/xsn/explorer/helpers/LedgerHelper.scala @@ -19,7 +19,8 @@ object LedgerHelper { block .transactions .map(_.string) - .map(TransactionLoader.get) + .map(TransactionLoader.getWithValues) .map(Transaction.fromRPC) + .map(_._1) } } diff --git a/server/test/com/xsn/explorer/helpers/TransactionLoader.scala b/server/test/com/xsn/explorer/helpers/TransactionLoader.scala index 9c63c98..547e3ff 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionLoader.scala +++ b/server/test/com/xsn/explorer/helpers/TransactionLoader.scala @@ -13,13 +13,27 @@ object TransactionLoader { json(txid).as[Transaction[TransactionVIN]] } + def getWithValues(txid: String): Transaction[TransactionVIN.HasValues] = { + val plain = json(txid).as[Transaction[TransactionVIN]] + val newVIN = plain.vin.flatMap { vin => + get(vin.txid.string) + .vout + .find(_.n == vin.voutIndex) + .flatMap { prev => + prev.address.map { vin.withValues(prev.value, _) } + } + } + + plain.copy(vin = newVIN) + } + def json(txid: String): JsValue = { try { val resource = s"$BasePath/$txid" val json = scala.io.Source.fromResource(resource).getLines().mkString("\n") Json.parse(json) } catch { - case _ => throw new RuntimeException(s"Transaction $txid not found") + case _: Throwable => throw new RuntimeException(s"Transaction $txid not found") } } diff --git a/server/test/com/xsn/explorer/models/persisted/TransactionSpec.scala b/server/test/com/xsn/explorer/models/persisted/TransactionSpec.scala new file mode 100644 index 0000000..e18adbc --- /dev/null +++ b/server/test/com/xsn/explorer/models/persisted/TransactionSpec.scala @@ -0,0 +1,97 @@ +package com.xsn.explorer.models.persisted + +import com.xsn.explorer.helpers.DataGenerator +import com.xsn.explorer.models._ +import com.xsn.explorer.models.rpc.ScriptPubKey +import com.xsn.explorer.models.values._ +import javax.xml.bind.DatatypeConverter +import org.scalatest.MustMatchers._ +import org.scalatest.OptionValues._ +import org.scalatest.WordSpec + +class TransactionSpec extends WordSpec { + + "HasIO" should { + "expect outputs matching the txid" in { + val tx = Transaction( + id = DataGenerator.randomTransactionId, + blockhash = DataGenerator.randomBlockhash, + size = Size(20), + time = 100L + ) + + val outputs = DataGenerator.randomOutputs(2) + intercept[RuntimeException] { + Transaction.HasIO( + tx, + inputs = List.empty, + outputs = outputs) + } + + val _ = Transaction.HasIO( + tx, + inputs = List.empty, + outputs = outputs.map(_.copy(txid = tx.id))) + } + } + + "fromRPC" should { + "discard outputs without address" in { + val address = DataGenerator.randomAddress + val hex = HexString.from("00").get + val vout = List( + rpc.TransactionVOUT(0, 1, None), + rpc.TransactionVOUT(10, 2, Some(ScriptPubKey( + "nulldata", + "", + hex, + List(address)))), + ) + + val tx = rpc.Transaction[rpc.TransactionVIN.HasValues]( + id = DataGenerator.randomTransactionId, + size = Size(200), + blockhash = DataGenerator.randomBlockhash, + time = 10L, + blocktime = 10L, + confirmations = Confirmations(10), + vin = List.empty, + vout = vout) + + val expected = Transaction.Output(tx.id, 2, 10, address, hex) + val (result, _) = persisted.Transaction.fromRPC(tx) + result.outputs must be(List(expected)) + } + + "extract the possible TPoS contracts" in { + val address = DataGenerator.randomAddress + val addressHex = DatatypeConverter.printHexBinary(address.string.getBytes) + val contractASM = s"OP_RETURN $addressHex $addressHex 90 aabbccff" + val script = ScriptPubKey("nulldata", contractASM, HexString.from("00").get, List.empty) + val voutWithContract = rpc.TransactionVOUT(value = 0, n = 1, Some(script)) + val collateral = rpc.TransactionVOUT( + n = 0, + value = 1 + ) + val tx = rpc.Transaction( + id = DataGenerator.randomTransactionId, + size = Size(200), + blockhash = DataGenerator.randomBlockhash, + time = 10L, + blocktime = 10L, + confirmations = Confirmations(10), + vin = List(), + vout = List(collateral, voutWithContract)).copy[rpc.TransactionVIN.HasValues](vin = List.empty) + + val expected = TPoSContract( + id = TPoSContract.Id(tx.id, collateral.n), + time = tx.time, + state = TPoSContract.State.Active, + details = TPoSContract.Details(address, address, TPoSContract.Commission.from(10).get) + ) + + val (_, contract) = persisted.Transaction.fromRPC(tx) + contract.value must be(expected) + } + } +}