You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
15 KiB
456 lines
15 KiB
package com.xsn.explorer.data
|
|
|
|
import com.alexitc.playsonify.models.ordering.{FieldOrdering, OrderingCondition}
|
|
import com.alexitc.playsonify.models.pagination._
|
|
import com.xsn.explorer.data.common.PostgresDataHandlerSpec
|
|
import com.xsn.explorer.errors.{BlockNotFoundError, TransactionNotFoundError}
|
|
import com.xsn.explorer.helpers.Converters._
|
|
import com.xsn.explorer.helpers.DataHandlerObjects._
|
|
import com.xsn.explorer.helpers.DataHelper._
|
|
import com.xsn.explorer.helpers.{DataGenerator, LedgerHelper, TransactionLoader}
|
|
import com.xsn.explorer.models._
|
|
import com.xsn.explorer.models.fields.TransactionField
|
|
import com.xsn.explorer.models.persisted.Transaction
|
|
import com.xsn.explorer.models.rpc.Block
|
|
import com.xsn.explorer.models.values.{Height, _}
|
|
import org.scalactic.{Bad, Good, One, Or}
|
|
import org.scalatest.BeforeAndAfter
|
|
|
|
class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with BeforeAndAfter {
|
|
|
|
import DataGenerator._
|
|
|
|
lazy val dataHandler = createTransactionDataHandler(database)
|
|
lazy val ledgerDataHandler = createLedgerDataHandler(database)
|
|
lazy val blockDataHandler = createBlockDataHandler(database)
|
|
|
|
val defaultOrdering = FieldOrdering(TransactionField.Time, OrderingCondition.DescendingOrder)
|
|
|
|
val blockList = LedgerHelper.blockList.take(6)
|
|
|
|
val block = DataGenerator.randomBlock()
|
|
|
|
val dummyTransaction = randomTransaction(blockhash = block.hash, utxos = List.empty)
|
|
|
|
before {
|
|
clearDatabase()
|
|
prepareBlock(block)
|
|
prepareTransaction(dummyTransaction)
|
|
}
|
|
|
|
"getBy address" should {
|
|
val address = randomAddress
|
|
val partialTransaction = randomTransaction(blockhash = block.hash, utxos = dummyTransaction.outputs)
|
|
val outputsForAddress = partialTransaction.outputs.map { _.copy(address = address) }
|
|
val transaction = partialTransaction.copy(outputs = outputsForAddress)
|
|
val query = PaginatedQuery(Offset(0), Limit(10))
|
|
|
|
"find no results" in {
|
|
val expected = PaginatedResult(query.offset, query.limit, Count(0), List.empty)
|
|
val result = dataHandler.getBy(randomAddress, query, defaultOrdering)
|
|
|
|
result mustEqual Good(expected)
|
|
}
|
|
|
|
"find the right values" in {
|
|
upsertTransaction(transaction)
|
|
|
|
val transactionWithValues = TransactionWithValues(
|
|
transaction.id,
|
|
transaction.blockhash,
|
|
transaction.time,
|
|
transaction.size,
|
|
received = transaction.outputs.filter(_.address == address).map(_.value).sum,
|
|
sent = transaction.inputs.filter(_.address == address).map(_.value).sum)
|
|
|
|
val expected = PaginatedResult(query.offset, query.limit, Count(1), List(transactionWithValues))
|
|
val result = dataHandler.getBy(address, query, defaultOrdering)
|
|
result mustEqual Good(expected)
|
|
}
|
|
}
|
|
|
|
"getUnspentOutputs" should {
|
|
"return non-zero results" in {
|
|
clearDatabase()
|
|
val blocks = blockList.take(3)
|
|
blocks.map(createBlock)
|
|
|
|
val expected = Transaction.Output(
|
|
address = createAddress("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"),
|
|
txid = createTransactionId("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8"),
|
|
index = 0,
|
|
value = BigDecimal(76500000),
|
|
script = HexString.from("2103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac").get
|
|
)
|
|
|
|
val result = dataHandler.getUnspentOutputs(expected.address).get
|
|
|
|
result mustEqual List(expected)
|
|
}
|
|
}
|
|
|
|
"getOutput" should {
|
|
val txid = createTransactionId("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8")
|
|
|
|
"return the output" in {
|
|
val index = 0
|
|
val expected = Transaction.Output(
|
|
address = createAddress("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"),
|
|
txid = createTransactionId("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8"),
|
|
index = 0,
|
|
value = BigDecimal(76500000.000000000000000),
|
|
script = HexString.from("2103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac").get
|
|
)
|
|
|
|
clearDatabase()
|
|
val blocks = blockList.take(3)
|
|
blocks.map(createBlock)
|
|
|
|
val result = dataHandler.getOutput(txid, index)
|
|
result must be(Good(expected))
|
|
}
|
|
|
|
"return no value" in {
|
|
val index = 10
|
|
|
|
val result = dataHandler.getOutput(txid, index)
|
|
result must be(Bad(TransactionNotFoundError).accumulating)
|
|
}
|
|
}
|
|
|
|
"getByBlockhash" should {
|
|
val blockhash = randomBlockhash
|
|
|
|
val transactions = List(
|
|
randomTransaction(blockhash = blockhash, utxos = dummyTransaction.outputs),
|
|
randomTransaction(blockhash = blockhash, utxos = dummyTransaction.outputs),
|
|
randomTransaction(blockhash = blockhash, utxos = dummyTransaction.outputs)
|
|
)
|
|
|
|
val block = randomBlock(blockhash = blockhash).copy(transactions = transactions.map(_.id))
|
|
|
|
"find no results" in {
|
|
val blockhash = randomBlockhash
|
|
val query = PaginatedQuery(Offset(0), Limit(10))
|
|
val expected = PaginatedResult(query.offset, query.limit, Count(0), List.empty)
|
|
val result = dataHandler.getByBlockhash(blockhash, query, defaultOrdering)
|
|
|
|
result mustEqual Good(expected)
|
|
}
|
|
|
|
"find the right values" in {
|
|
createBlock(block, transactions)
|
|
|
|
val query = PaginatedQuery(Offset(0), Limit(10))
|
|
val result = dataHandler.getByBlockhash(blockhash, query, defaultOrdering).get
|
|
|
|
result.total mustEqual Count(transactions.size)
|
|
result.offset mustEqual query.offset
|
|
result.limit mustEqual query.limit
|
|
result.data.size mustEqual transactions.size
|
|
}
|
|
|
|
def testOrdering[B](field: TransactionField, orderingCondition: OrderingCondition)(lt: (Transaction.HasIO, Transaction.HasIO) => Boolean) = {
|
|
createBlock(block, transactions)
|
|
|
|
val ordering = FieldOrdering(field, orderingCondition)
|
|
val query = PaginatedQuery(Offset(0), Limit(10))
|
|
|
|
val expected = transactions.sortWith(lt).map(_.id)
|
|
val result = dataHandler.getByBlockhash(blockhash, query, ordering).get.data
|
|
|
|
result.map(_.id) mustEqual expected
|
|
}
|
|
|
|
"allow to sort by txid" in {
|
|
testOrdering(TransactionField.TransactionId, OrderingCondition.AscendingOrder) { case (a, b) => a.id.string.compareTo(b.id.string) < 0 }
|
|
}
|
|
|
|
"allow to sort by txid - descending" in {
|
|
testOrdering(TransactionField.TransactionId, OrderingCondition.DescendingOrder) { case (a, b) => a.id.string.compareTo(b.id.string) > 0 }
|
|
}
|
|
|
|
"allow to sort by time" in {
|
|
testOrdering(TransactionField.Time, OrderingCondition.AscendingOrder) { case (a, b) =>
|
|
if (a.time < b.time) true
|
|
else if (a.time > b.time) false
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
"allow to sort by time - descending" in {
|
|
testOrdering(TransactionField.Time, OrderingCondition.DescendingOrder) { case (a, b) =>
|
|
if (a.time < b.time) false
|
|
else if (a.time > b.time) true
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
"allow to sort by sent" in {
|
|
testOrdering(TransactionField.Sent, OrderingCondition.AscendingOrder) { case (a, b) =>
|
|
if (a.inputs.map(_.value).sum < b.inputs.map(_.value).sum) true
|
|
else if (a.inputs.map(_.value).sum > b.inputs.map(_.value).sum) false
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
"allow to sort by sent - descending" in {
|
|
testOrdering(TransactionField.Sent, OrderingCondition.DescendingOrder) { case (a, b) =>
|
|
if (a.inputs.map(_.value).sum < b.inputs.map(_.value).sum) false
|
|
else if (a.inputs.map(_.value).sum > b.inputs.map(_.value).sum) true
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
"allow to sort by received" in {
|
|
testOrdering(TransactionField.Received, OrderingCondition.AscendingOrder) { case (a, b) =>
|
|
if (a.outputs.map(_.value).sum < b.outputs.map(_.value).sum) true
|
|
else if (a.outputs.map(_.value).sum > b.outputs.map(_.value).sum) false
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
"allow to sort by received - descending" in {
|
|
testOrdering(TransactionField.Received, OrderingCondition.DescendingOrder) { case (a, b) =>
|
|
if (a.outputs.map(_.value).sum < b.outputs.map(_.value).sum) false
|
|
else if (a.outputs.map(_.value).sum > b.outputs.map(_.value).sum) true
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
}
|
|
|
|
"getBy with scroll" should {
|
|
val address = randomAddress
|
|
val blockhash = randomBlockhash
|
|
val inputs = List(
|
|
Transaction.Input(dummyTransaction.id, 0, 1, 100, address),
|
|
Transaction.Input(dummyTransaction.id, 1, 2, 200, address)
|
|
)
|
|
|
|
val outputs = List(
|
|
Transaction.Output(randomTransactionId, 0, BigDecimal(50), randomAddress, randomHexString()),
|
|
Transaction.Output(
|
|
randomTransactionId,
|
|
1,
|
|
BigDecimal(250),
|
|
randomAddress,
|
|
HexString.from("00").get)
|
|
)
|
|
|
|
val transactions = List.fill(4)(randomTransactionId).zip(List(321L, 320L, 319L, 319L)).map { case (txid, time) =>
|
|
Transaction.HasIO(
|
|
Transaction(
|
|
txid,
|
|
blockhash,
|
|
time,
|
|
Size(1000)),
|
|
inputs,
|
|
outputs.map(_.copy(txid = txid)))
|
|
}
|
|
|
|
val block = randomBlock(blockhash = blockhash).copy(transactions = transactions.map(_.id))
|
|
|
|
def prepare() = {
|
|
createBlock(block, transactions)
|
|
}
|
|
|
|
def testOrdering[B](tag: String, condition: OrderingCondition) = {
|
|
val sorted = condition match {
|
|
case OrderingCondition.AscendingOrder =>
|
|
transactions
|
|
.sortWith { case (a, b) =>
|
|
if (a.time < b.time) true
|
|
else if (a.time > b.time) false
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
|
|
case OrderingCondition.DescendingOrder =>
|
|
transactions
|
|
.sortWith { case (a, b) =>
|
|
if (a.time > b.time) true
|
|
else if (a.time < b.time) false
|
|
else a.id.string.compareTo(b.id.string) < 0
|
|
}
|
|
}
|
|
|
|
def matchOnlyData(expected: Transaction.HasIO, actual: Transaction.HasIO) = {
|
|
actual.copy(inputs = List.empty, outputs = List.empty) mustEqual expected.copy(inputs = List.empty, outputs = List.empty)
|
|
}
|
|
|
|
s"[$tag] return the first elements" in {
|
|
prepare()
|
|
val expected = sorted.head
|
|
val result = dataHandler.getBy(address, Limit(1), None, condition).get
|
|
|
|
matchOnlyData(expected, result.head)
|
|
}
|
|
|
|
s"[$tag] return the next elements given the last seen tx" in {
|
|
prepare()
|
|
|
|
val lastSeenTxid = sorted.head.id
|
|
val expected = sorted(1)
|
|
val result = dataHandler.getBy(address, Limit(1), Option(lastSeenTxid), condition).get
|
|
matchOnlyData(expected, result.head)
|
|
}
|
|
|
|
s"[$tag] return the element with the same time breaking ties by txid" in {
|
|
prepare()
|
|
|
|
val lastSeenTxid = sorted(2).id
|
|
val expected = sorted(3)
|
|
val result = dataHandler.getBy(address, Limit(1), Option(lastSeenTxid), condition).get
|
|
matchOnlyData(expected, result.head)
|
|
}
|
|
|
|
s"[$tag] return no elements on unknown lastSeenTransaction" in {
|
|
val lastSeenTxid = createTransactionId("00041e4fe89466faa734d6207a7ef6115fa1dd33f7156b006ffff6bb85a79eb8")
|
|
val result = dataHandler.getBy(address, Limit(1), Option(lastSeenTxid), condition).get
|
|
result must be(empty)
|
|
}
|
|
}
|
|
|
|
testOrdering("desc", OrderingCondition.DescendingOrder)
|
|
testOrdering("asc", OrderingCondition.AscendingOrder)
|
|
}
|
|
|
|
"spending an output" should {
|
|
"use the right values" in {
|
|
val address = randomAddress
|
|
val blockhash = randomBlockhash
|
|
val inputs = List(
|
|
Transaction.Input(dummyTransaction.id, 0, 1, 100, address),
|
|
Transaction.Input(dummyTransaction.id, 1, 2, 200, address)
|
|
)
|
|
|
|
val newTxid = randomTransactionId
|
|
val outputs = List(
|
|
Transaction.Output(newTxid, 0, BigDecimal(50), randomAddress, randomHexString()),
|
|
Transaction.Output(
|
|
newTxid,
|
|
1,
|
|
BigDecimal(250),
|
|
randomAddress,
|
|
randomHexString())
|
|
)
|
|
|
|
val transaction = Transaction.HasIO(
|
|
Transaction(
|
|
newTxid,
|
|
blockhash,
|
|
321,
|
|
Size(1000)),
|
|
inputs,
|
|
outputs)
|
|
|
|
val newTxid2 = randomTransactionId
|
|
val newAddress = randomAddress
|
|
val transaction2 = transaction.copy(
|
|
transaction = transaction.transaction.copy(id = newTxid2),
|
|
inputs = List(
|
|
Transaction.Input(
|
|
fromTxid = transaction.id,
|
|
fromOutputIndex = 0,
|
|
index = 0,
|
|
value = transaction.outputs(0).value,
|
|
address = newAddress),
|
|
Transaction.Input(
|
|
fromTxid = transaction.id,
|
|
fromOutputIndex = 1,
|
|
index = 1,
|
|
value = transaction.outputs(1).value,
|
|
address = newAddress)
|
|
),
|
|
outputs = transaction.outputs.map(_.copy(txid = newTxid2))
|
|
)
|
|
|
|
val transactions = List(
|
|
transaction, transaction2)
|
|
|
|
val block = this.block.copy(
|
|
hash = blockhash,
|
|
height = Height(10),
|
|
transactions = transactions.map(_.id))
|
|
|
|
createBlock(block, transactions)
|
|
|
|
// check that the outputs are properly spent
|
|
database.withConnection { implicit conn =>
|
|
import _root_.anorm._
|
|
|
|
val spentOn = SQL(
|
|
s"""
|
|
|SELECT spent_on
|
|
|FROM transaction_outputs
|
|
|WHERE txid = '${transaction.id.string}'
|
|
""".stripMargin
|
|
).as(SqlParser.str("spent_on").*)
|
|
|
|
spentOn.foreach(_ mustEqual transaction2.id.string)
|
|
}
|
|
|
|
// check that the inputs are linked to the correct output
|
|
database.withConnection { implicit conn =>
|
|
import _root_.anorm._
|
|
|
|
val query = SQL(
|
|
s"""
|
|
|SELECT from_txid, from_output_index
|
|
|FROM transaction_inputs
|
|
|WHERE txid = '${transaction2.id.string}'
|
|
""".stripMargin
|
|
)
|
|
|
|
val fromTxid = query.as(SqlParser.str("from_txid").*)
|
|
fromTxid.foreach(_ mustEqual transaction.id.string)
|
|
|
|
val fromOutputIndex = query.as(SqlParser.int("from_output_index").*)
|
|
fromOutputIndex.sorted mustEqual List(0, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
private def createBlock(block: Block) = {
|
|
val transactions = block.transactions
|
|
.map(_.string)
|
|
.map(TransactionLoader.getWithValues)
|
|
.map(Transaction.fromRPC)
|
|
.map(_._1)
|
|
|
|
val result = ledgerDataHandler.push(block.withTransactions(transactions), List.empty)
|
|
|
|
result.isGood mustEqual true
|
|
}
|
|
|
|
private def createBlock(block: Block, transactions: List[Transaction.HasIO]) = {
|
|
val result = ledgerDataHandler.push(block.withTransactions(transactions), List.empty)
|
|
|
|
result.isGood mustEqual true
|
|
}
|
|
|
|
private def prepareBlock(block: Block) = {
|
|
try {
|
|
database.withConnection { implicit conn =>
|
|
val maybe = blockPostgresDAO.insert(block)
|
|
Or.from(maybe, One(BlockNotFoundError))
|
|
}
|
|
} catch {
|
|
case _: Throwable => ()
|
|
}
|
|
}
|
|
|
|
private def prepareTransaction(transaction: Transaction.HasIO) = {
|
|
try {
|
|
upsertTransaction(transaction)
|
|
} catch {
|
|
case _: Throwable => ()
|
|
}
|
|
}
|
|
|
|
private def upsertTransaction(transaction: Transaction.HasIO) = {
|
|
database.withConnection { implicit conn =>
|
|
val maybe = transactionPostgresDAO.upsert(1, transaction)
|
|
Or.from(maybe, One(TransactionNotFoundError))
|
|
}.isGood must be(true)
|
|
}
|
|
}
|
|
|