From ee71562fb6054b89dd6caf4df641ee615c024b21 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sun, 1 Jul 2018 22:00:32 -0500 Subject: [PATCH] server: Add getUnspentOutputs method to the TransactionDataHandler (#34) --- .../data/TransactionDataHandler.scala | 2 + .../TransactionPostgresDataHandler.scala | 5 ++ .../anorm/dao/TransactionPostgresDAO.scala | 14 ++++ .../anorm/parsers/TransactionParsers.scala | 12 ++-- .../async/TransactionFutureDataHandler.scala | 4 ++ .../com/xsn/explorer/models/HexString.scala | 5 +- .../com/xsn/explorer/models/Transaction.scala | 3 +- .../TransactionPostgresDataHandlerSpec.scala | 64 +++++++++++++++++-- .../helpers/TransactionDummyDataHandler.scala | 2 + 9 files changed, 99 insertions(+), 12 deletions(-) diff --git a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala index 39599dd..492cccb 100644 --- a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala +++ b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala @@ -13,6 +13,8 @@ trait TransactionDataHandler[F[_]] { address: Address, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]] + + def getUnspentOutputs(address: Address): F[List[Transaction.Output]] } trait TransactionBlockingDataHandler extends TransactionDataHandler[ApplicationResult] diff --git a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala index 4d5d206..b6d340b 100644 --- a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala @@ -28,4 +28,9 @@ class TransactionPostgresDataHandler @Inject() ( Good(result) } + + def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = withConnection { implicit conn => + val result = transactionPostgresDAO.getUnspentOutputs(address) + Good(result) + } } diff --git a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala index 04ae538..4d9fbba 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala @@ -167,6 +167,20 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi Count(result) } + def getUnspentOutputs(address: Address)(implicit conn: Connection): List[Transaction.Output] = { + SQL( + """ + |SELECT txid, index, value, address, hex_script, tpos_owner_address, tpos_merchant_address + |FROM transaction_outputs + |WHERE address = {address} AND + | spent_on IS NULL AND + | value > 0 + """.stripMargin + ).on( + 'address -> address.string + ).as(parseTransactionOutput.*).flatten + } + private def upsertTransaction(transaction: Transaction)(implicit conn: Connection): Option[Transaction] = { SQL( """ diff --git a/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala b/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala index f6eebbb..adea489 100644 --- a/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala +++ b/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala @@ -54,15 +54,19 @@ object TransactionParsers { } val parseTransactionOutput = ( - parseIndex ~ + parseTransactionId ~ + parseIndex ~ parseValue ~ parseAddress ~ parseHexString ~ parseTposOwnerAddress.? ~ parseTposMerchantAddress.?).map { - case index ~ value ~ addressMaybe ~ scriptMaybe ~ tposOwnerAddress ~ tposMerchantAddress => - for (address <- addressMaybe; script <- scriptMaybe) - yield Transaction.Output(index, value, address, script, tposOwnerAddress.flatten, tposMerchantAddress.flatten) + case txidMaybe ~ index ~ value ~ addressMaybe ~ scriptMaybe ~ tposOwnerAddress ~ tposMerchantAddress => + for { + txid <- txidMaybe + address <- addressMaybe + script <- scriptMaybe + } yield Transaction.Output(txid, index, value, address, script, tposOwnerAddress.flatten, tposMerchantAddress.flatten) } } diff --git a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala index c3494af..45fa68e 100644 --- a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala @@ -23,4 +23,8 @@ class TransactionFutureDataHandler @Inject() ( blockingDataHandler.getBy(address, paginatedQuery, ordering) } + + override def getUnspentOutputs(address: Address): FutureApplicationResult[List[Transaction.Output]] = Future { + blockingDataHandler.getUnspentOutputs(address) + } } diff --git a/server/app/com/xsn/explorer/models/HexString.scala b/server/app/com/xsn/explorer/models/HexString.scala index d049c37..1056059 100644 --- a/server/app/com/xsn/explorer/models/HexString.scala +++ b/server/app/com/xsn/explorer/models/HexString.scala @@ -1,6 +1,9 @@ package com.xsn.explorer.models -class HexString private (val string: String) extends AnyVal +class HexString private (val string: String) extends AnyVal { + + override def toString: String = string +} object HexString { diff --git a/server/app/com/xsn/explorer/models/Transaction.scala b/server/app/com/xsn/explorer/models/Transaction.scala index 710a5e1..06d720c 100644 --- a/server/app/com/xsn/explorer/models/Transaction.scala +++ b/server/app/com/xsn/explorer/models/Transaction.scala @@ -21,6 +21,7 @@ object Transaction { address: Address) case class Output( + txid: TransactionId, index: Int, value: BigDecimal, address: Address, @@ -48,7 +49,7 @@ object Transaction { for { address <- vout.address script <- scriptMaybe - } yield Transaction.Output(vout.n, vout.value, address, script, tposAddresses.map(_._1), tposAddresses.map(_._2)) + } yield Transaction.Output(tx.id, vout.n, vout.value, address, script, tposAddresses.map(_._1), tposAddresses.map(_._2)) } Transaction( diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index 7ef1fba..14afbeb 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala @@ -1,12 +1,13 @@ package com.xsn.explorer.data import com.alexitc.playsonify.models._ -import com.xsn.explorer.data.anorm.dao.{BlockPostgresDAO, TransactionPostgresDAO} +import com.xsn.explorer.data.anorm.dao.{BalancePostgresDAO, BlockPostgresDAO, TransactionPostgresDAO} import com.xsn.explorer.data.anorm.interpreters.FieldOrderingSQLInterpreter -import com.xsn.explorer.data.anorm.{BlockPostgresDataHandler, TransactionPostgresDataHandler} +import com.xsn.explorer.data.anorm.{BlockPostgresDataHandler, LedgerPostgresDataHandler, TransactionPostgresDataHandler} import com.xsn.explorer.data.common.PostgresDataHandlerSpec import com.xsn.explorer.errors.{BlockNotFoundError, TransactionNotFoundError} import com.xsn.explorer.helpers.DataHelper._ +import com.xsn.explorer.helpers.{BlockLoader, TransactionLoader} import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField import com.xsn.explorer.models.rpc.Block @@ -16,6 +17,12 @@ import org.scalatest.BeforeAndAfter class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with BeforeAndAfter { lazy val dataHandler = new TransactionPostgresDataHandler(database, new TransactionPostgresDAO(new FieldOrderingSQLInterpreter)) + lazy val ledgerDataHandler = new LedgerPostgresDataHandler( + database, + new BlockPostgresDAO, + new TransactionPostgresDAO(new FieldOrderingSQLInterpreter), + new BalancePostgresDAO(new FieldOrderingSQLInterpreter)) + lazy val blockDataHandler = new BlockPostgresDataHandler(database, new BlockPostgresDAO) val defaultOrdering = FieldOrdering(TransactionField.Time, OrderingCondition.DescendingOrder) @@ -45,7 +52,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be Size(1000), List.empty, List( - Transaction.Output(0, 1000, createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7Dkpss"), HexString.from("00").get, None, None) + Transaction.Output(createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 0, 1000, createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7Dkpss"), HexString.from("00").get, None, None) ) ) @@ -54,8 +61,9 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be ) val outputs = List( - Transaction.Output(0, BigDecimal(50), createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F"), HexString.from("00").get, None, None), + Transaction.Output(createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 0, BigDecimal(50), createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F"), HexString.from("00").get, None, None), Transaction.Output( + createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 1, BigDecimal(150), createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"), @@ -70,7 +78,16 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be 12312312L, Size(1000), inputs, - outputs) + outputs.map(_.copy(txid = createTransactionId("99c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8")))) + + val blockList = List( + BlockLoader.get("00000c822abdbb23e28f79a49d29b41429737c6c7e15df40d1b1f1b35907ae34"), + BlockLoader.get("000003fb382f6892ae96594b81aa916a8923c70701de4e7054aac556c7271ef7"), + BlockLoader.get("000004645e2717b556682e3c642a4c6e473bf25c653ff8e8c114a3006040ffb8"), + BlockLoader.get("00000766115b26ecbc09cd3a3db6870fdaf2f049d65a910eb2f2b48b566ca7bd"), + BlockLoader.get("00000b59875e80b0afc6c657bc5318d39e03532b7d97fb78a4c7bd55c4840c32"), + BlockLoader.get("00000267225f7dba55d9a3493740e7f0dde0f28a371d2c3b42e7676b5728d020") + ) private def prepareBlock(block: Block) = { val dao = new BlockPostgresDAO @@ -171,8 +188,9 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be ) val outputs = List( - Transaction.Output(0, BigDecimal(50), address, HexString.from("00").get, None, None), + Transaction.Output(createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 0, BigDecimal(50), address, HexString.from("00").get, None, None), Transaction.Output( + createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 1, BigDecimal(250), createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"), @@ -210,4 +228,38 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be 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, + tposMerchantAddress = None, + tposOwnerAddress = None + ) + + val result = dataHandler.getUnspentOutputs(expected.address).get + result.size mustEqual 1 + + result mustEqual List(expected) + } + } + + private def createBlock(block: Block) = { + val transactions = block.transactions + .map(_.string) + .map(TransactionLoader.get) + .map(Transaction.fromRPC) + + val result = ledgerDataHandler.push(block, transactions) + + result.isGood mustEqual true + } } diff --git a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala index b1e70f6..37a3d30 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala +++ b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala @@ -9,4 +9,6 @@ import com.xsn.explorer.models.fields.TransactionField class TransactionDummyDataHandler extends TransactionBlockingDataHandler { override def getBy(address: Address, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = ??? + + override def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = ??? }