Browse Source

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.
master
Alexis Hernandez 6 years ago
parent
commit
80f02664e5
  1. 35
      server/app/com/xsn/explorer/models/persisted/Transaction.scala
  2. 2
      server/app/com/xsn/explorer/services/TransactionRPCService.scala
  3. 3
      server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala
  4. 3
      server/test/com/xsn/explorer/helpers/LedgerHelper.scala
  5. 16
      server/test/com/xsn/explorer/helpers/TransactionLoader.scala
  6. 97
      server/test/com/xsn/explorer/models/persisted/TransactionSpec.scala

35
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
)
}
}

2
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
}

3
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))

3
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)
}
}

16
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")
}
}

97
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)
}
}
}
Loading…
Cancel
Save