From 87b345dd76ac57d3e87a3471d448973b64f466f2 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sun, 28 Apr 2019 17:18:58 -0600 Subject: [PATCH] server: Refactor the transaction models to support multisig transactions This refactor changes the input/output model from transactions to store the possibly empty address list, which allows to deal with certain transactions on bitcoin as well as with multisig transactions. --- .../anorm/LedgerPostgresDataHandler.scala | 26 ++-- ...AddressTransactionDetailsPostgresDAO.scala | 24 ++-- .../dao/TransactionInputPostgresDAO.scala | 14 +- .../dao/TransactionOutputPostgresDAO.scala | 20 +-- .../data/anorm/parsers/CommonParsers.scala | 14 +- .../anorm/parsers/TransactionParsers.scala | 12 +- .../models/LightWalletTransaction.scala | 2 +- .../explorer/models/TransactionDetails.scala | 19 ++- .../explorer/models/TransactionValue.scala | 27 ++-- .../xsn/explorer/models/persisted/Block.scala | 4 +- .../models/persisted/Transaction.scala | 41 +++++- .../explorer/models/rpc/TransactionVIN.scala | 21 ++- .../explorer/models/rpc/TransactionVOUT.scala | 2 +- .../TransactionCollectorService.scala | 4 +- .../services/TransactionRPCService.scala | 123 ++---------------- .../explorer/services/logic/BlockLogic.scala | 22 ++-- .../services/logic/TransactionLogic.scala | 2 +- .../app/controllers/AddressesController.scala | 27 +++- server/conf/evolutions/default/18.sql | 26 ++++ .../TransactionPostgresDataHandlerSpec.scala | 8 +- .../helpers/FileBasedXSNService.scala | 5 + .../explorer/helpers/TransactionLoader.scala | 2 +- .../controllers/AddressesControllerSpec.scala | 2 +- .../TransactionsControllerSpec.scala | 66 +++------- ...b6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6 | 62 +++++++++ ...2b7da30f0c10e1411ba2d30973afb41e489e0993fe | 62 +++++++++ ...85bf29c35af2b9e32c5fc792dd098840219fe6354a | 62 +++++++++ ...6d7e89f26b3816580bed1712da4b4e60913efc7b34 | 62 +++++++++ ...4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c | 62 +++++++++ ...d26151e9bd2b517d8b35ac111016b4c890a1f78a8d | 62 +++++++++ ...b61e3bad6185fd8e3137d63c1960e4df67088f3fe5 | 62 +++++++++ ...f39de17dcaf6418adb521f5ff9d29c13239d50bcba | 62 +++++++++ ...79859b029be069cc8ea6fc6480dfda40b64d828c6c | 62 +++++++++ ...f636d27871ab99bd69031668fafc0ae6cbe4f9941a | 62 +++++++++ ...fb3465dda7864acbdb9885f519217dd147178b7188 | 62 +++++++++ ...df6f6f321409b8b00b6020ce977e45746a09f964ad | 88 +++++++++++++ 36 files changed, 1019 insertions(+), 264 deletions(-) create mode 100644 server/conf/evolutions/default/18.sql create mode 100644 server/test/resources/transactions/100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6 create mode 100644 server/test/resources/transactions/1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe create mode 100644 server/test/resources/transactions/56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a create mode 100644 server/test/resources/transactions/63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34 create mode 100644 server/test/resources/transactions/7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c create mode 100644 server/test/resources/transactions/980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d create mode 100644 server/test/resources/transactions/a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5 create mode 100644 server/test/resources/transactions/b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba create mode 100644 server/test/resources/transactions/bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c create mode 100644 server/test/resources/transactions/da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a create mode 100644 server/test/resources/transactions/e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188 create mode 100644 server/test/resources/transactions/f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad diff --git a/server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala index 39d30ce..9624bb5 100644 --- a/server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala @@ -132,23 +132,25 @@ class LedgerPostgresDataHandler @Inject() ( } private def spendMap(transactions: List[Transaction.HasIO]): Map[Address, BigDecimal] = { - transactions - .map(_.inputs) - .flatMap { inputs => - inputs.map { input => input.address -> input.value } - } + val addressValueList = for { + tx <- transactions + input <- tx.inputs + address <- input.addresses + } yield address -> input.value + + addressValueList .groupBy(_._1) .mapValues { list => list.map(_._2).sum } } private def receiveMap(transactions: List[Transaction.HasIO]): Map[Address, BigDecimal] = { - transactions - .map(_.outputs) - .flatMap { outputs => - outputs.map { output => - output.address -> output.value - } - } + val addressValueList = for { + tx <- transactions + output <- tx.outputs + address <- output.addresses + } yield address -> output.value + + addressValueList .groupBy(_._1) .mapValues { list => list.map(_._2).sum } } diff --git a/server/app/com/xsn/explorer/data/anorm/dao/AddressTransactionDetailsPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/AddressTransactionDetailsPostgresDAO.scala index e32e259..8907ec5 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/AddressTransactionDetailsPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/AddressTransactionDetailsPostgresDAO.scala @@ -10,16 +10,24 @@ import com.xsn.explorer.models.values.TransactionId class AddressTransactionDetailsPostgresDAO { def batchInsertDetails(transaction: Transaction.HasIO)(implicit conn: Connection): Option[Unit] = { - val received = transaction - .outputs - .groupBy(_.address) - .mapValues { outputs => outputs.map(_.value).sum } + val outputAddressValueList = for { + output <- transaction.outputs + address <- output.addresses + } yield address -> output.value + + val received = outputAddressValueList + .groupBy(_._1) + .mapValues { _.map(_._2).sum } .map { case (address, value) => AddressTransactionDetails(address, transaction.id, time = transaction.time, received = value) } - val sent = transaction - .inputs - .groupBy(_.address) - .mapValues { inputs => inputs.map(_.value).sum } + val inputAddressValueList = for { + input <- transaction.inputs + address <- input.addresses + } yield address -> input.value + + val sent = inputAddressValueList + .groupBy(_._1) + .mapValues { _.map(_._2).sum } .map { case (address, value) => AddressTransactionDetails(address, transaction.id, time = transaction.time, sent = value) } val details = (received ++ sent) diff --git a/server/app/com/xsn/explorer/data/anorm/dao/TransactionInputPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/TransactionInputPostgresDAO.scala index 7210768..2f91395 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionInputPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionInputPostgresDAO.scala @@ -27,15 +27,15 @@ class TransactionInputPostgresDAO { 'from_txid -> input.fromTxid.string: NamedParameter, 'from_output_index -> input.fromOutputIndex: NamedParameter, 'value -> input.value: NamedParameter, - 'address -> input.address.string: NamedParameter) + 'addresses -> input.addresses.map(_.string): NamedParameter) } val batch = BatchSql( """ |INSERT INTO transaction_inputs - | (txid, index, from_txid, from_output_index, value, address) + | (txid, index, from_txid, from_output_index, value, addresses) |VALUES - | ({txid}, {index}, {from_txid}, {from_output_index}, {value}, {address}) + | ({txid}, {index}, {from_txid}, {from_output_index}, {value}, ARRAY[{addresses}]) """.stripMargin, params.head, params.tail: _* @@ -55,7 +55,7 @@ class TransactionInputPostgresDAO { """ |DELETE FROM transaction_inputs |WHERE txid = {txid} - |RETURNING txid, index, from_txid, from_output_index, value, address + |RETURNING txid, index, from_txid, from_output_index, value, addresses """.stripMargin ).on( 'txid -> txid.string @@ -65,7 +65,7 @@ class TransactionInputPostgresDAO { def getInputs(txid: TransactionId)(implicit conn: Connection): List[Transaction.Input] = { SQL( """ - |SELECT txid, index, from_txid, from_output_index, value, address + |SELECT txid, index, from_txid, from_output_index, value, addresses |FROM transaction_inputs |WHERE txid = {txid} """.stripMargin @@ -77,10 +77,10 @@ class TransactionInputPostgresDAO { def getInputs(txid: TransactionId, address: Address)(implicit conn: Connection): List[Transaction.Input] = { SQL( """ - |SELECT txid, index, from_txid, from_output_index, value, address + |SELECT txid, index, from_txid, from_output_index, value, addresses |FROM transaction_inputs |WHERE txid = {txid} AND - | address = {address} + | {address} = ANY(addresses) """.stripMargin ).on( 'txid -> txid.string, diff --git a/server/app/com/xsn/explorer/data/anorm/dao/TransactionOutputPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/TransactionOutputPostgresDAO.scala index da8541c..51fdcbc 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionOutputPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionOutputPostgresDAO.scala @@ -15,9 +15,9 @@ class TransactionOutputPostgresDAO { def getUnspentOutputs(address: Address)(implicit conn: Connection): List[Transaction.Output] = { SQL( """ - |SELECT txid, index, value, address, hex_script + |SELECT txid, index, value, addresses, hex_script |FROM transaction_outputs - |WHERE address = {address} AND + |WHERE {address} = ANY(addresses) AND | spent_on IS NULL AND | value > 0 """.stripMargin @@ -29,7 +29,7 @@ class TransactionOutputPostgresDAO { def getOutput(txid: TransactionId, index: Int)(implicit conn: Connection): Option[Transaction.Output] = { SQL( """ - |SELECT txid, index, value, address, hex_script + |SELECT txid, index, value, addresses, hex_script |FROM transaction_outputs |WHERE txid = {txid} AND | index = {index} @@ -52,16 +52,16 @@ class TransactionOutputPostgresDAO { 'txid -> output.txid.string: NamedParameter, 'index -> output.index: NamedParameter, 'value -> output.value: NamedParameter, - 'address -> output.address.string: NamedParameter, + 'addresses -> output.addresses.map(_.string): NamedParameter, 'hex_script -> output.script.string: NamedParameter) } val batch = BatchSql( """ |INSERT INTO transaction_outputs - | (txid, index, value, address, hex_script) + | (txid, index, value, addresses, hex_script) |VALUES - | ({txid}, {index}, {value}, {address}, {hex_script}) + | ({txid}, {index}, {value}, ARRAY[{addresses}], {hex_script}) """.stripMargin, params.head, params.tail: _* @@ -81,7 +81,7 @@ class TransactionOutputPostgresDAO { """ |DELETE FROM transaction_outputs |WHERE txid = {txid} - |RETURNING txid, index, hex_script, value, address + |RETURNING txid, index, hex_script, value, addresses """.stripMargin ).on( 'txid -> txid.string @@ -93,7 +93,7 @@ class TransactionOutputPostgresDAO { def getOutputs(txid: TransactionId)(implicit conn: Connection): List[Transaction.Output] = { SQL( """ - |SELECT txid, index, hex_script, value, address + |SELECT txid, index, hex_script, value, addresses |FROM transaction_outputs |WHERE txid = {txid} """.stripMargin @@ -105,10 +105,10 @@ class TransactionOutputPostgresDAO { def getOutputs(txid: TransactionId, address: Address)(implicit conn: Connection): List[Transaction.Output] = { SQL( """ - |SELECT txid, index, hex_script, value, address + |SELECT txid, index, hex_script, value, addresses |FROM transaction_outputs |WHERE txid = {txid} AND - | address = {address} + | {address} = ANY(addresses) """.stripMargin ).on( 'txid -> txid.string, diff --git a/server/app/com/xsn/explorer/data/anorm/parsers/CommonParsers.scala b/server/app/com/xsn/explorer/data/anorm/parsers/CommonParsers.scala index 07f9eb7..d0b2cf8 100644 --- a/server/app/com/xsn/explorer/data/anorm/parsers/CommonParsers.scala +++ b/server/app/com/xsn/explorer/data/anorm/parsers/CommonParsers.scala @@ -1,6 +1,6 @@ package com.xsn.explorer.data.anorm.parsers -import anorm.SqlParser.{int, long, str} +import anorm.SqlParser._ import com.xsn.explorer.models.values._ object CommonParsers { @@ -13,6 +13,18 @@ object CommonParsers { .map(Address.from) .map { _.getOrElse(throw new RuntimeException(s"corrupted $field")) } + def parseAddresses = array[String]("addresses") + .map { array => + array + .map { string => + Address.from(string) match { + case None => throw new RuntimeException("Corrupted address") + case Some(address) => address + } + } + .toList + } + def parseTransactionId(field: String = "txid") = str(field) .map(TransactionId.from) .map { _.getOrElse(throw new RuntimeException(s"corrupted $field")) } 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 eb61758..d3a1e29 100644 --- a/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala +++ b/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala @@ -33,20 +33,20 @@ object TransactionParsers { TransactionWithValues(txid, blockhash, time, size, sent, received) } - val parseTransactionInput = (parseFromTxid ~ parseFromOutputIndex ~ parseIndex ~ parseValue ~ parseAddress()) - .map { case fromTxid ~ fromOutputIndex ~ index ~ value ~ address => - Transaction.Input(fromTxid, fromOutputIndex, index, value, address) + val parseTransactionInput = (parseFromTxid ~ parseFromOutputIndex ~ parseIndex ~ parseValue ~ parseAddresses) + .map { case fromTxid ~ fromOutputIndex ~ index ~ value ~ addresses => + Transaction.Input(fromTxid, fromOutputIndex, index, value, addresses) } val parseTransactionOutput = ( parseTransactionId() ~ parseIndex ~ parseValue ~ - parseAddress() ~ + parseAddresses ~ parseHexScript).map { - case txid ~ index ~ value ~ address ~ script => - Transaction.Output(txid, index, value, address, script) + case txid ~ index ~ value ~ addresses ~ script => + Transaction.Output(txid, index, value, addresses, script) } val parseAddressTransactionDetails = (parseAddress() ~ parseTransactionId() ~ parseSent ~ parseReceived ~ parseTime).map { diff --git a/server/app/com/xsn/explorer/models/LightWalletTransaction.scala b/server/app/com/xsn/explorer/models/LightWalletTransaction.scala index d5de207..7f493a0 100644 --- a/server/app/com/xsn/explorer/models/LightWalletTransaction.scala +++ b/server/app/com/xsn/explorer/models/LightWalletTransaction.scala @@ -13,6 +13,6 @@ case class LightWalletTransaction( object LightWalletTransaction { case class Input(txid: TransactionId, index: Int, value: BigDecimal) - case class Output(index: Int, value: BigDecimal, address: Address) + case class Output(index: Int, value: BigDecimal, addresses: List[Address]) } diff --git a/server/app/com/xsn/explorer/models/TransactionDetails.scala b/server/app/com/xsn/explorer/models/TransactionDetails.scala index 5338483..3131745 100644 --- a/server/app/com/xsn/explorer/models/TransactionDetails.scala +++ b/server/app/com/xsn/explorer/models/TransactionDetails.scala @@ -1,7 +1,7 @@ package com.xsn.explorer.models import com.xsn.explorer.models.values.{Blockhash, Confirmations, Size, TransactionId} -import play.api.libs.json.{Json, Writes} +import play.api.libs.json.{JsValue, Json, Writes} case class TransactionDetails( id: TransactionId, @@ -24,7 +24,7 @@ object TransactionDetails { def from(tx: rpc.Transaction[rpc.TransactionVIN.HasValues]): TransactionDetails = { val input = tx.vin.map { vin => - TransactionValue(vin.address, vin.value) + TransactionValue(vin.addresses, vin.value) } val output = tx.vout.flatMap(TransactionValue.from) @@ -32,5 +32,18 @@ object TransactionDetails { TransactionDetails(tx.id, tx.size, tx.blockhash, tx.time, tx.blocktime, tx.confirmations, input, output) } - implicit val writes: Writes[TransactionDetails] = Json.writes[TransactionDetails] + implicit val writes: Writes[TransactionDetails] = new Writes[TransactionDetails] { + override def writes(o: TransactionDetails): JsValue = { + Json.obj( + "id" -> o.id, + "size" -> o.size, + "blockhash" -> o.blockhash, + "time" -> o.time, + "blocktime" -> o.blocktime, + "confirmations" -> o.confirmations, + "input" -> o.input, + "output" -> o.output + ) + } + } } diff --git a/server/app/com/xsn/explorer/models/TransactionValue.scala b/server/app/com/xsn/explorer/models/TransactionValue.scala index 25c5a55..6913af8 100644 --- a/server/app/com/xsn/explorer/models/TransactionValue.scala +++ b/server/app/com/xsn/explorer/models/TransactionValue.scala @@ -2,20 +2,29 @@ package com.xsn.explorer.models import com.xsn.explorer.models.rpc.TransactionVOUT import com.xsn.explorer.models.values.Address -import play.api.libs.json.{Json, Writes} +import play.api.libs.json.{JsNull, JsString, Json, Writes} -case class TransactionValue(address: Address, value: BigDecimal) +case class TransactionValue(addresses: List[Address], value: BigDecimal) { + def address: Option[Address] = addresses.headOption +} object TransactionValue { - def from(vout: TransactionVOUT): Option[TransactionValue] = { - val value = vout.value - val addressMaybe = vout.address + def apply(address: Address, value: BigDecimal): TransactionValue = { + TransactionValue(List(address), value) + } - addressMaybe.map { address => - TransactionValue(address, value) - } + def from(vout: TransactionVOUT): Option[TransactionValue] = { + for (addresses <- vout.addresses) + yield TransactionValue(addresses, vout.value) } - implicit val writes: Writes[TransactionValue] = Json.writes[TransactionValue] + implicit val writes: Writes[TransactionValue] = (obj: TransactionValue) => { + val address = obj.address.map(_.string).map(JsString.apply).getOrElse(JsNull) + Json.obj( + "address" -> address, // Keeps compatibility + "addresses" -> obj.addresses, + "value" -> obj.value + ) + } } diff --git a/server/app/com/xsn/explorer/models/persisted/Block.scala b/server/app/com/xsn/explorer/models/persisted/Block.scala index 982769a..d1b7438 100644 --- a/server/app/com/xsn/explorer/models/persisted/Block.scala +++ b/server/app/com/xsn/explorer/models/persisted/Block.scala @@ -43,8 +43,8 @@ object Block { */ def collectAddresses: Set[Address] = { transactions.foldLeft(Set.empty[Address]) { case (acc, tx) => - val spending = tx.inputs.map(_.address) - val receiving = tx.outputs.map(_.address) + val spending = tx.inputs.flatMap(_.addresses) + val receiving = tx.outputs.flatMap(_.addresses) spending.toSet ++ receiving.toSet ++ acc } } diff --git a/server/app/com/xsn/explorer/models/persisted/Transaction.scala b/server/app/com/xsn/explorer/models/persisted/Transaction.scala index 5de4002..514c125 100644 --- a/server/app/com/xsn/explorer/models/persisted/Transaction.scala +++ b/server/app/com/xsn/explorer/models/persisted/Transaction.scala @@ -20,14 +20,43 @@ object Transaction { fromOutputIndex: Int, index: Int, value: BigDecimal, - address: Address) + addresses: List[Address]) { + + def address: Option[Address] = addresses.headOption + } + object Input { + def apply( + fromTxid: TransactionId, + fromOutputIndex: Int, + index: Int, + value: BigDecimal, + address: Address): Input = { + + Input(fromTxid, fromOutputIndex, index, value, List(address)) + } + } case class Output( txid: TransactionId, index: Int, value: BigDecimal, - address: Address, - script: HexString) + addresses: List[Address], + script: HexString) { + + def address: Option[Address] = addresses.headOption + } + + object Output { + def apply( + txid: TransactionId, + index: Int, + value: BigDecimal, + address: Address, + script: HexString): Output = { + + new Output(txid, index, value, List(address), script) + } + } case class HasIO( transaction: Transaction, @@ -55,18 +84,18 @@ object Transaction { .vin .zipWithIndex .map { case (vin, index) => - Transaction.Input(vin.txid, vin.voutIndex, index, vin.value, vin.address) + Transaction.Input(vin.txid, vin.voutIndex, index, vin.value, vin.addresses) } val outputs = tx.vout.flatMap { vout => for { - address <- vout.address + addresses <- vout.addresses script <- vout.scriptPubKey.map(_.hex) } yield Output( tx.id, vout.n, vout.value, - address, + addresses, script) } diff --git a/server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala b/server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala index fe3b907..0029548 100644 --- a/server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala +++ b/server/app/com/xsn/explorer/models/rpc/TransactionVIN.scala @@ -8,8 +8,12 @@ sealed trait TransactionVIN { def txid: TransactionId def voutIndex: Int + def withValues(value: BigDecimal, addresses: List[Address]): TransactionVIN.HasValues = { + TransactionVIN.HasValues(txid, voutIndex, value, addresses) + } + def withValues(value: BigDecimal, address: Address): TransactionVIN.HasValues = { - TransactionVIN.HasValues(txid, voutIndex, value, address) + TransactionVIN.HasValues(txid, voutIndex, value, List(address)) } } @@ -20,21 +24,14 @@ object TransactionVIN { override val txid: TransactionId, override val voutIndex: Int, value: BigDecimal, - address: Address) extends TransactionVIN + addresses: List[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, valueMaybe, addressMaybe) => - val maybe = for { - value <- valueMaybe - address <- addressMaybe - } yield HasValues(txid, index, value, address) + (__ \ 'vout).read[Int] - maybe.getOrElse(Raw(txid, index)) + builder.apply { (txid, index) => + Raw(txid, index) } } } diff --git a/server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala b/server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala index 14d47b7..7aad5c2 100644 --- a/server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala +++ b/server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala @@ -9,7 +9,7 @@ case class TransactionVOUT( n: Int, scriptPubKey: Option[ScriptPubKey] = None) { - val address: Option[Address] = scriptPubKey.flatMap(_.addresses.headOption) + val addresses: Option[List[Address]] = scriptPubKey.map(_.addresses) } object TransactionVOUT { diff --git a/server/app/com/xsn/explorer/services/TransactionCollectorService.scala b/server/app/com/xsn/explorer/services/TransactionCollectorService.scala index ecd5bfa..1dd2b57 100644 --- a/server/app/com/xsn/explorer/services/TransactionCollectorService.scala +++ b/server/app/com/xsn/explorer/services/TransactionCollectorService.scala @@ -86,7 +86,7 @@ class TransactionCollectorService @Inject() ( .getOutput(vin.txid, vin.voutIndex) .toFutureOr .map { output => - vin.withValues(value = output.value, address = output.address) + vin.withValues(value = output.value, addresses = output.addresses) } .toFuture .map(vin -> _) @@ -182,7 +182,7 @@ class TransactionCollectorService @Inject() ( transactionValue.map { case Good(value) => - val newVIN = vin.withValues(value = value.value, address = value.address) + val newVIN = vin.withValues(value = value.value, addresses = value.addresses) Good(newVIN) case Bad(e) => Bad(e) diff --git a/server/app/com/xsn/explorer/services/TransactionRPCService.scala b/server/app/com/xsn/explorer/services/TransactionRPCService.scala index 9704689..b44a65e 100644 --- a/server/app/com/xsn/explorer/services/TransactionRPCService.scala +++ b/server/app/com/xsn/explorer/services/TransactionRPCService.scala @@ -1,24 +1,21 @@ package com.xsn.explorer.services -import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureListOps, FutureOps, OrOps} -import com.alexitc.playsonify.core.{FutureApplicationResult, FutureOr} -import com.xsn.explorer.errors.{InvalidRawTransactionError, TransactionNotFoundError, XSNWorkQueueDepthExceeded} -import com.xsn.explorer.models.persisted.Transaction -import com.xsn.explorer.models.rpc.TransactionVIN +import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} +import com.xsn.explorer.errors.InvalidRawTransactionError +import com.xsn.explorer.models.TransactionDetails import com.xsn.explorer.models.values._ -import com.xsn.explorer.models.{TPoSContract, TransactionDetails, TransactionValue} import com.xsn.explorer.services.validators.TransactionIdValidator -import com.xsn.explorer.util.Extensions.FutureOrExt import javax.inject.Inject -import org.scalactic.{Bad, Good, One, Or} +import org.scalactic.{One, Or} import org.slf4j.LoggerFactory import play.api.libs.json.{JsString, JsValue, Json} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.control.NonFatal +import scala.concurrent.ExecutionContext class TransactionRPCService @Inject() ( transactionIdValidator: TransactionIdValidator, + transactionCollectorService: TransactionCollectorService, xsnService: XSNService)( implicit ec: ExecutionContext) { @@ -37,88 +34,12 @@ class TransactionRPCService @Inject() ( val result = for { txid <- transactionIdValidator.validate(txidString).toFutureOr transaction <- xsnService.getTransaction(txid).toFutureOr - vin <- getTransactionVIN(transaction.vin).toFutureOr + vin <- transactionCollectorService.getRPCTransactionVIN(transaction.vin, List.empty).toFutureOr } yield TransactionDetails.from(transaction.copy(vin = vin)) result.toFuture } - def getTransaction(txid: TransactionId): FutureApplicationResult[(Transaction.HasIO, Option[TPoSContract])] = { - val result = for { - tx <- xsnService.getTransaction(txid).toFutureOr - transactionVIN <- getTransactionVIN(tx.vin).toFutureOr - rpcTransaction = tx.copy(vin = transactionVIN) - } yield Transaction.fromRPC(rpcTransaction) - - result.toFuture - } - - private def getTransactionVIN(list: List[TransactionVIN]): FutureApplicationResult[List[TransactionVIN.HasValues]] = { - def getVIN(vin: TransactionVIN) = { - getTransactionValue(vin) - .map { - case Good(transactionValue) => - 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.HasValues]] = pending match { - case x :: xs => - for { - tx <- getVIN(x).toFutureOr - next <- loadVINSequentially(xs) - } yield tx :: next - - case _ => Future.successful(Good(List.empty)).toFutureOr - } - - list - .map(getVIN) - .toFutureOr - .toFuture - .recoverWith { - case NonFatal(ex) => - logger.warn(s"Failed to load VIN, trying sequentially, error = ${ex.getMessage}") - loadVINSequentially(list).toFuture - } - } - - def getTransactions(ids: List[TransactionId]): FutureApplicationResult[(List[Transaction.HasIO], List[TPoSContract])] = { - def loadTransactionsSlowly(pending: List[TransactionId]): FutureOr[List[(Transaction.HasIO, Option[TPoSContract])]] = pending match { - case x :: xs => - for { - tx <- getTransaction(x).toFutureOr - next <- loadTransactionsSlowly(xs) - } yield tx :: next - - case _ => Future.successful(Good(List.empty)).toFutureOr - } - - ids - .map(getTransaction) - .toFutureOr - .recoverWith(XSNWorkQueueDepthExceeded) { - logger.warn("Unable to load transaction due to server overload, loading them slowly") - loadTransactionsSlowly(ids) - } - .toFuture - .recoverWith { - case NonFatal(ex) => - logger.warn(s"Unable to load transactions due to server error, loading them sequentially, error = ${ex.getMessage}") - loadTransactionsSlowly(ids).toFuture - } - .toFutureOr - .map { result => - val contracts = result.flatMap(_._2) - val txs = result.map(_._1) - (txs, contracts) - } - .toFuture - } - def sendRawTransaction(hexString: String): FutureApplicationResult[JsValue] = { val result = for { hex <- Or.from(HexString.from(hexString), One(InvalidRawTransactionError)).toFutureOr @@ -127,32 +48,4 @@ class TransactionRPCService @Inject() ( result.toFuture } - - private def getTransactionValue(vin: TransactionVIN): FutureApplicationResult[TransactionValue] = { - val valueMaybe = vin match { - case x: TransactionVIN.HasValues => Some(TransactionValue(x.address, x.value)) - case _ => None - } - - valueMaybe - .map(Good(_)) - .map(Future.successful) - .getOrElse { - val txid = vin.txid - - val result = for { - tx <- xsnService.getTransaction(txid).toFutureOr - r <- { - val maybe = tx - .vout - .find(_.n == vin.voutIndex) - .flatMap(TransactionValue.from) - - Or.from(maybe, One(TransactionNotFoundError)).toFutureOr - } - } yield r - - result.toFuture - } - } } diff --git a/server/app/com/xsn/explorer/services/logic/BlockLogic.scala b/server/app/com/xsn/explorer/services/logic/BlockLogic.scala index 449c0bd..00cc20d 100644 --- a/server/app/com/xsn/explorer/services/logic/BlockLogic.scala +++ b/server/app/com/xsn/explorer/services/logic/BlockLogic.scala @@ -77,7 +77,7 @@ class BlockLogic { val coinstakeVOUT = coinstakeTx.vout.drop(1) if (coinstakeVOUT.size >= 1 && coinstakeVOUT.size <= 3) { val value = coinstakeVOUT - .filter(_.address contains coinstakeAddress) + .filter(_.addresses.getOrElse(List.empty) contains coinstakeAddress) .map(_.value) .sum @@ -85,12 +85,12 @@ class BlockLogic { coinstakeAddress, (value - coinstakeInput) max 0) - val masternodeRewardOUT = coinstakeVOUT.filterNot(_.address contains coinstakeAddress) - val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.address).headOption + val masternodeRewardOUT = coinstakeVOUT.filterNot(_.addresses.getOrElse(List.empty) contains coinstakeAddress) + val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.addresses).flatten.headOption val masternodeRewardMaybe = masternodeAddressMaybe.map { masternodeAddress => BlockReward( masternodeAddress, - masternodeRewardOUT.filter(_.address contains masternodeAddress).map(_.value).sum + masternodeRewardOUT.filter(_.addresses.getOrElse(List.empty) contains masternodeAddress).map(_.value).sum ) } @@ -117,7 +117,7 @@ class BlockLogic { val coinstakeVOUT = coinstakeTx.vout val ownerValue = coinstakeVOUT - .filter(_.address contains contract.owner) + .filter(_.addresses.getOrElse(List.empty) contains contract.owner) .map(_.value) .sum @@ -126,19 +126,19 @@ class BlockLogic { (ownerValue - coinstakeInput) max 0) // merchant - val merchantValue = coinstakeVOUT.filter(_.address contains contract.merchant).map(_.value).sum + val merchantValue = coinstakeVOUT.filter(_.addresses.getOrElse(List.empty) contains contract.merchant).map(_.value).sum val merchantReward = BlockReward(contract.merchant, merchantValue) // master node val masternodeRewardOUT = coinstakeVOUT.filterNot { out => - out.address.contains(contract.owner) || - out.address.contains(contract.merchant) + out.addresses.getOrElse(List.empty).contains(contract.owner) || + out.addresses.getOrElse(List.empty).contains(contract.merchant) } - val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.address).headOption + val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.addresses.getOrElse(List.empty)).headOption val masternodeRewardMaybe = masternodeAddressMaybe.map { masternodeAddress => BlockReward( masternodeAddress, - masternodeRewardOUT.filter(_.address contains masternodeAddress).map(_.value).sum + masternodeRewardOUT.filter(_.addresses.getOrElse(List.empty) contains masternodeAddress).map(_.value).sum ) } @@ -148,6 +148,6 @@ class BlockLogic { def isPoS(block: rpc.Block, coinbase: rpc.Transaction[_]): Boolean = { block.nonce == 0 && coinbase.vin.isEmpty && - coinbase.vout.flatMap(_.address).isEmpty + coinbase.vout.flatMap(_.addresses.getOrElse(List.empty)).isEmpty } } diff --git a/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala b/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala index 8b33aa8..9a4735f 100644 --- a/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala +++ b/server/app/com/xsn/explorer/services/logic/TransactionLogic.scala @@ -9,7 +9,7 @@ import org.scalactic.{One, Or} class TransactionLogic { def getAddress(vout: TransactionVOUT, error: ApplicationError): ApplicationResult[Address] = { - val maybe = vout.address + val maybe = vout.addresses.flatMap(_.headOption) Or.from(maybe, One(error)) } diff --git a/server/app/controllers/AddressesController.scala b/server/app/controllers/AddressesController.scala index bb6c6c6..6d49002 100644 --- a/server/app/controllers/AddressesController.scala +++ b/server/app/controllers/AddressesController.scala @@ -47,8 +47,16 @@ class AddressesController @Inject() ( * Format to keep compatibility with the previous approach using the RPC api. */ implicit private val writes: Writes[Transaction.Output] = Writes { obj => + val address = obj + .addresses + .headOption + .map(_.string) + .map(JsString.apply) + .getOrElse(JsNull) + val values = Map( - "address" -> JsString(obj.address.string), + "address" -> address, // Keep compatibility with the legacy API + "addresses" -> JsArray(obj.addresses.map(_.string).map(JsString.apply)), "txid" -> JsString(obj.txid.string), "script" -> JsString(obj.script.string), "outputIndex" -> JsNumber(obj.index), @@ -70,7 +78,22 @@ class AddressesController @Inject() ( object AddressesController { implicit val inputWrites: Writes[LightWalletTransaction.Input] = Json.writes[LightWalletTransaction.Input] - implicit val outputWrites: Writes[LightWalletTransaction.Output] = Json.writes[LightWalletTransaction.Output] + implicit val outputWrites: Writes[LightWalletTransaction.Output] = (obj: LightWalletTransaction.Output) => { + val address = obj + .addresses + .headOption + .map(_.string) + .map(JsString.apply) + .getOrElse(JsNull) + + Json.obj( + "index" -> obj.index, + "value" -> obj.value, + "address" -> address, + "addresses" -> obj.addresses + ) + } + implicit val lightWalletTransactionWrites: Writes[LightWalletTransaction] = Json.writes[LightWalletTransaction] } diff --git a/server/conf/evolutions/default/18.sql b/server/conf/evolutions/default/18.sql new file mode 100644 index 0000000..c0e1beb --- /dev/null +++ b/server/conf/evolutions/default/18.sql @@ -0,0 +1,26 @@ + +# --- !Ups + +ALTER TABLE transaction_outputs + ALTER COLUMN address TYPE TEXT[] USING ARRAY[address::TEXT]; +ALTER TABLE transaction_outputs + RENAME address TO addresses; + + +ALTER TABLE transaction_inputs + ALTER COLUMN address TYPE TEXT[] USING ARRAY[address::TEXT]; +ALTER TABLE transaction_inputs + RENAME address TO addresses; + + +# --- !Downs + +ALTER TABLE transaction_outputs + ALTER COLUMN addresses TYPE ADDRESS_TYPE USING addresses[1]::ADDRESS_TYPE; +ALTER TABLE transaction_outputs + RENAME addresses TO address; + +ALTER TABLE transaction_inputs + ALTER COLUMN addresses TYPE ADDRESS_TYPE USING addresses[1]::ADDRESS_TYPE; +ALTER TABLE transaction_inputs + RENAME addresses TO address; diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index e72f859..c19413d 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala @@ -41,7 +41,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be "getBy address" should { val address = randomAddress val partialTransaction = randomTransaction(blockhash = block.hash, utxos = dummyTransaction.outputs) - val outputsForAddress = partialTransaction.outputs.map { _.copy(address = address) } + val outputsForAddress = partialTransaction.outputs.map { _.copy(addresses = List(address)) } val transaction = partialTransaction.copy(outputs = outputsForAddress) val query = PaginatedQuery(Offset(0), Limit(10)) @@ -60,8 +60,8 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be transaction.blockhash, transaction.time, transaction.size, - received = transaction.outputs.filter(_.address == address).map(_.value).sum, - sent = transaction.inputs.filter(_.address == address).map(_.value).sum) + received = transaction.outputs.filter(_.address contains address).map(_.value).sum, + sent = transaction.inputs.filter(_.address contains address).map(_.value).sum) val expected = PaginatedResult(query.offset, query.limit, Count(1), List(transactionWithValues)) val result = dataHandler.getBy(address, query, defaultOrdering) @@ -83,7 +83,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be script = HexString.from("2103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac").get ) - val result = dataHandler.getUnspentOutputs(expected.address).get + val result = dataHandler.getUnspentOutputs(expected.address.get).get result mustEqual List(expected) } diff --git a/server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala b/server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala index 1523e59..d26d4dc 100644 --- a/server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala +++ b/server/test/com/xsn/explorer/helpers/FileBasedXSNService.scala @@ -44,4 +44,9 @@ class FileBasedXSNService extends DummyXSNService { val result = Or.from(maybe, One(TransactionNotFoundError)) Future.successful(result) } + + override def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = { + val tx = TransactionLoader.json(txid.string) + Future.successful(Good(tx)) + } } diff --git a/server/test/com/xsn/explorer/helpers/TransactionLoader.scala b/server/test/com/xsn/explorer/helpers/TransactionLoader.scala index 547e3ff..67f151b 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionLoader.scala +++ b/server/test/com/xsn/explorer/helpers/TransactionLoader.scala @@ -20,7 +20,7 @@ object TransactionLoader { .vout .find(_.n == vin.voutIndex) .flatMap { prev => - prev.address.map { vin.withValues(prev.value, _) } + prev.addresses.map { vin.withValues(prev.value, _) } } } diff --git a/server/test/controllers/AddressesControllerSpec.scala b/server/test/controllers/AddressesControllerSpec.scala index d4bb4b9..d9567b9 100644 --- a/server/test/controllers/AddressesControllerSpec.scala +++ b/server/test/controllers/AddressesControllerSpec.scala @@ -118,7 +118,7 @@ class AddressesControllerSpec extends MyAPISpec { def url(address: String) = s"/addresses/$address/utxos" def matches(json: JsValue, output: Transaction.Output) = { - (json \ "address").as[String] mustEqual output.address.string + (json \ "address").as[String] mustEqual output.address.map(_.string).getOrElse("") (json \ "txid").as[String] mustEqual output.txid.string (json \ "outputIndex").as[Int] mustEqual output.index (json \ "script").as[String] mustEqual output.script.string diff --git a/server/test/controllers/TransactionsControllerSpec.scala b/server/test/controllers/TransactionsControllerSpec.scala index 075ddd3..3726b55 100644 --- a/server/test/controllers/TransactionsControllerSpec.scala +++ b/server/test/controllers/TransactionsControllerSpec.scala @@ -1,67 +1,35 @@ package controllers -import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.play.PublicErrorRenderer 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.helpers.{DataHelper, FileBasedXSNService, TransactionDummyDataHandler, TransactionLoader} import com.xsn.explorer.models._ -import com.xsn.explorer.models.rpc.{Transaction, TransactionVIN} -import com.xsn.explorer.models.values.{Confirmations, Size, TransactionId} +import com.xsn.explorer.models.values._ import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec -import org.scalactic.{Bad, Good} +import org.scalactic.Bad import play.api.inject.bind import play.api.libs.json.JsValue import play.api.test.Helpers._ -import scala.concurrent.Future - class TransactionsControllerSpec extends MyAPISpec { import DataHelper._ - val coinbaseTx = TransactionLoader.get("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c") - val nonCoinbaseTx = TransactionLoader.get("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641") - val nonCoinbasePreviousTx = TransactionLoader.get("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9") - val severalInputsTx = TransactionLoader.get("a3c43d22bbba31a6e5c00f565cb9c5a1a365407df4cc90efa8a865656b52c0eb") - val firstAddress = createAddress("Xvjue2ZLnJwTrSLUBx7DTHaSHTdpWrxtLF") - val secondAddress = createAddress("bc1qzhayf65p2j4h3pfw22aujgr5w42xfqzx5uvddt") - val firstTxId = DataHelper.createTransactionId("a3c43d223658a8656a31a6e5c407df4bbb0f565cb9c5a1acc90efa056b52c0eb") - val secondTxId = DataHelper.createTransactionId("8a865656b5a3c43d22b00f565cb9c5a1a3bba31a6e5c65407df4cc90efa2c0eb") - - val customXSNService = new DummyXSNService { - val map = Map( - coinbaseTx.id -> coinbaseTx, - nonCoinbaseTx.id -> nonCoinbaseTx, - nonCoinbasePreviousTx.id -> nonCoinbasePreviousTx, - severalInputsTx.id -> severalInputsTx - ) - - override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction[TransactionVIN]] = { - val result = map.get(txid) - .map(Good(_)) - .getOrElse { - Bad(TransactionNotFoundError).accumulating - } - - Future.successful(result) - } + private val coinbaseTx = TransactionLoader.get("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c") + private val nonCoinbaseTx = TransactionLoader.get("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641") + private val severalInputsTx = TransactionLoader.get("a3c43d22bbba31a6e5c00f565cb9c5a1a365407df4cc90efa8a865656b52c0eb") - override def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = { - val result = map.get(txid) - .map { _ => TransactionLoader.json(txid.string) } - .map(Good(_)) - .getOrElse { - Bad(TransactionNotFoundError).accumulating - } + private val customXSNService = new FileBasedXSNService - Future.successful(result) + private val transactionDataHandler = new TransactionDummyDataHandler { + override def getOutput(txid: TransactionId, index: Int): ApplicationResult[persisted.Transaction.Output] = { + Bad(TransactionNotFoundError).accumulating } } - val transactionDataHandler = new TransactionDummyDataHandler {} - override val application = guiceApplicationBuilder .overrides(bind[XSNService].to(customXSNService)) .overrides(bind[TransactionBlockingDataHandler].to(transactionDataHandler)) @@ -87,7 +55,7 @@ class TransactionsControllerSpec extends MyAPISpec { outputJsonList.size mustEqual 1 val outputJson = outputJsonList.head - (outputJson \ "address").as[String] mustEqual tx.vout.head.address.get.string + (outputJson \ "address").as[String] mustEqual tx.vout.head.addresses.flatMap(_.headOption).get.string (outputJson \ "value").as[BigDecimal] mustEqual tx.vout.head.value } @@ -97,7 +65,7 @@ class TransactionsControllerSpec extends MyAPISpec { createAddress("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL"), BigDecimal("2343749.965625"))) .map { v => - TransactionVIN.HasValues(createTransactionId("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c"), 0, v.value, v.address) + rpc.TransactionVIN.HasValues(createTransactionId("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c"), 0, v.value, v.addresses) } val tx = nonCoinbaseTx.copy(vin = input) @@ -118,18 +86,18 @@ class TransactionsControllerSpec extends MyAPISpec { inputJsonList.size mustEqual 1 val inputJson = inputJsonList.head - (inputJson \ "address").as[String] mustEqual details.input.head.address.string + (inputJson \ "address").as[String] mustEqual details.input.head.address.map(_.string).getOrElse("") (inputJson \ "value").as[BigDecimal] mustEqual details.input.head.value val outputJsonList = (json \ "output").as[List[JsValue]] outputJsonList.size mustEqual 2 val outputJson = outputJsonList.head - (outputJson \ "address").as[String] mustEqual details.output.head.address.string + (outputJson \ "address").as[String] mustEqual details.output.head.address.map(_.string).getOrElse("") (outputJson \ "value").as[BigDecimal] mustEqual details.output.head.value val outputJson2 = outputJsonList.drop(1).head - (outputJson2 \ "address").as[String] mustEqual details.output.drop(1).head.address.string + (outputJson2 \ "address").as[String] mustEqual details.output.drop(1).head.address.map(_.string).getOrElse("") (outputJson2 \ "value").as[BigDecimal] mustEqual details.output.drop(1).head.value } @@ -154,7 +122,7 @@ class TransactionsControllerSpec extends MyAPISpec { inputJsonList.size mustEqual 11 inputJsonList.foreach { inputJson => - (inputJson \ "address").as[String] mustEqual inputValue.address.string + (inputJson \ "address").as[String] mustEqual inputValue.address.map(_.string).getOrElse("") (inputJson \ "value").as[BigDecimal] mustEqual inputValue.value } diff --git a/server/test/resources/transactions/100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6 b/server/test/resources/transactions/100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6 new file mode 100644 index 0000000..079fb77 --- /dev/null +++ b/server/test/resources/transactions/100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6 @@ -0,0 +1,62 @@ +{ + "txid": "100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6", + "hash": "100e0b7b5bf835afc6459fb6fe2ed51a68bd5b5b01e5c08f937f8b15879eeca6", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "135fb9e9d96586d97fcd8e79cef1bd253bb49d00131355b5c8c1c70f9c757dd2", + "vout": 2, + "scriptSig": { + "asm": "304502210099e54963723e0db2fd4209ecb0d8d7bd71b0da4ec2fa59b32ccfabb6b7d3490f022030561a6d3d7e6e11b467392336db7bb83f37b48ef1944570af1c88d54133d511[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "48304502210099e54963723e0db2fd4209ecb0d8d7bd71b0da4ec2fa59b32ccfabb6b7d3490f022030561a6d3d7e6e11b467392336db7bb83f37b48ef1944570af1c88d54133d511012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "0100000001d27d759c0fc7c1c8b5551313009db43b25bdf1ce798ecd7fd98665d9e9b95f13020000006b48304502210099e54963723e0db2fd4209ecb0d8d7bd71b0da4ec2fa59b32ccfabb6b7d3490f022030561a6d3d7e6e11b467392336db7bb83f37b48ef1944570af1c88d54133d511012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "28fb72e618e737b086143eefaf6d859117ecf354e98347da8be7c382c3ddddce", + "confirmations": 588069, + "time": 1520366729, + "blocktime": 1520366729 +} \ No newline at end of file diff --git a/server/test/resources/transactions/1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe b/server/test/resources/transactions/1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe new file mode 100644 index 0000000..45a45d5 --- /dev/null +++ b/server/test/resources/transactions/1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe @@ -0,0 +1,62 @@ +{ + "txid": "1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe", + "hash": "1361d3574b9091557e62732b7da30f0c10e1411ba2d30973afb41e489e0993fe", + "version": 1, + "size": 234, + "vsize": 234, + "weight": 936, + "locktime": 0, + "vin": [ + { + "txid": "533ddceab3774b117c81c994663f69a40cebae0a9d80db6ff64db8fe2a97b051", + "vout": 1, + "scriptSig": { + "asm": "304402200a0825834e6f912c26854a851e59d171c9fd839f7e61644dfe6588370da0657202202c6f9e08ffc6e9bb9b287157a66d4fb14e663524d8e5018bda555f5352fc4c9d[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "47304402200a0825834e6f912c26854a851e59d171c9fd839f7e61644dfe6588370da0657202202c6f9e08ffc6e9bb9b287157a66d4fb14e663524d8e5018bda555f5352fc4c9d012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "010000000151b0972afeb84df66fdb809d0aaeeb0ca4693f6694c9817c114b77b3eadc3d53010000006a47304402200a0825834e6f912c26854a851e59d171c9fd839f7e61644dfe6588370da0657202202c6f9e08ffc6e9bb9b287157a66d4fb14e663524d8e5018bda555f5352fc4c9d012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "fd067f15e759fd17dde4501cf755518be8b85eb77ed3f28fdd240dbc29841337", + "confirmations": 587629, + "time": 1520393339, + "blocktime": 1520393339 +} \ No newline at end of file diff --git a/server/test/resources/transactions/56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a b/server/test/resources/transactions/56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a new file mode 100644 index 0000000..33e3698 --- /dev/null +++ b/server/test/resources/transactions/56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a @@ -0,0 +1,62 @@ +{ + "txid": "56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a", + "hash": "56e1844d9d01a4586e903e85bf29c35af2b9e32c5fc792dd098840219fe6354a", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "815382b900ea1aa08df29d7caa7235714c23bddfcc7f5c1429790bf4ef855381", + "vout": 2, + "scriptSig": { + "asm": "3045022100fb79c4a782e839cac2e1ee305c362032ac02cedcbe0fad91e6e3557c6e79f12402205fc8e3983a5289654ff5c5f5704568bd00bb972c6124a277c51db45c8924a2c0[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "483045022100fb79c4a782e839cac2e1ee305c362032ac02cedcbe0fad91e6e3557c6e79f12402205fc8e3983a5289654ff5c5f5704568bd00bb972c6124a277c51db45c8924a2c0012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "0100000001815385eff40b7929145c7fccdfbd234c713572aa7c9df28da01aea00b9825381020000006b483045022100fb79c4a782e839cac2e1ee305c362032ac02cedcbe0fad91e6e3557c6e79f12402205fc8e3983a5289654ff5c5f5704568bd00bb972c6124a277c51db45c8924a2c0012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "b7a006c1b8bbf858f42dc7d7906151c5f074e1297f78eb4afa08ddd05f2daf24", + "confirmations": 587762, + "time": 1520384997, + "blocktime": 1520384997 +} \ No newline at end of file diff --git a/server/test/resources/transactions/63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34 b/server/test/resources/transactions/63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34 new file mode 100644 index 0000000..77fc67a --- /dev/null +++ b/server/test/resources/transactions/63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34 @@ -0,0 +1,62 @@ +{ + "txid": "63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34", + "hash": "63539693bf1d8df5d969e96d7e89f26b3816580bed1712da4b4e60913efc7b34", + "version": 1, + "size": 234, + "vsize": 234, + "weight": 936, + "locktime": 0, + "vin": [ + { + "txid": "67bd941a90e99db9be6cdb2551249cf0ec9732b5cc8ed5d7917795b5f43af64f", + "vout": 2, + "scriptSig": { + "asm": "304402201f444235e301daf80909e2bd8a7d8ff9fad883ed6005c85a365a6b1526e21afc022031e4a0a60abff969f9b09eebf36468bd60b4e48db4ef60d4df346ea4787e4c47[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "47304402201f444235e301daf80909e2bd8a7d8ff9fad883ed6005c85a365a6b1526e21afc022031e4a0a60abff969f9b09eebf36468bd60b4e48db4ef60d4df346ea4787e4c47012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "01000000014ff63af4b5957791d7d58eccb53297ecf09c245125db6cbeb99de9901a94bd67020000006a47304402201f444235e301daf80909e2bd8a7d8ff9fad883ed6005c85a365a6b1526e21afc022031e4a0a60abff969f9b09eebf36468bd60b4e48db4ef60d4df346ea4787e4c47012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "dee3fab1c4e5187f6e42771f1cee64adbc67eca095d74a372f43a05022298a58", + "confirmations": 587635, + "time": 1520392854, + "blocktime": 1520392854 +} \ No newline at end of file diff --git a/server/test/resources/transactions/7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c b/server/test/resources/transactions/7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c new file mode 100644 index 0000000..b63d012 --- /dev/null +++ b/server/test/resources/transactions/7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c @@ -0,0 +1,62 @@ +{ + "txid": "7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c", + "hash": "7943aeb6eee0f92406930b4a1b34a8801477c2bf0d94fa7f2e7a43d61a9cd60c", + "version": 1, + "size": 234, + "vsize": 234, + "weight": 936, + "locktime": 0, + "vin": [ + { + "txid": "1a68bd141ff69bf579f3c128433dd77da98a75e39b37760558226cb2add78741", + "vout": 1, + "scriptSig": { + "asm": "304402201aee4cbc775a3b1354fe6456ac9f0688e0d73a6b3520f9f110c627a94955e07f0220101b3a44d24e6b6b05112011fca6bd8803239809659d5970c9f65e69c4f0bb93[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "47304402201aee4cbc775a3b1354fe6456ac9f0688e0d73a6b3520f9f110c627a94955e07f0220101b3a44d24e6b6b05112011fca6bd8803239809659d5970c9f65e69c4f0bb93012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "01000000014187d7adb26c22580576379be3758aa97dd73d4328c1f379f59bf61f14bd681a010000006a47304402201aee4cbc775a3b1354fe6456ac9f0688e0d73a6b3520f9f110c627a94955e07f0220101b3a44d24e6b6b05112011fca6bd8803239809659d5970c9f65e69c4f0bb93012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "5efbab10c6c38003cb27db443b51b00c00483c2db603a22c047ed699b3c804e1", + "confirmations": 588053, + "time": 1520367738, + "blocktime": 1520367738 +} \ No newline at end of file diff --git a/server/test/resources/transactions/980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d b/server/test/resources/transactions/980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d new file mode 100644 index 0000000..3f56048 --- /dev/null +++ b/server/test/resources/transactions/980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d @@ -0,0 +1,62 @@ +{ + "txid": "980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d", + "hash": "980dc3eb7577a44d9a57c6d26151e9bd2b517d8b35ac111016b4c890a1f78a8d", + "version": 1, + "size": 234, + "vsize": 234, + "weight": 936, + "locktime": 0, + "vin": [ + { + "txid": "533ddceab3774b117c81c994663f69a40cebae0a9d80db6ff64db8fe2a97b051", + "vout": 2, + "scriptSig": { + "asm": "304402206f75863b212819912e871f9071825d0274b8bdb4d736bad4c86ad95a6f3497a2022066145d851d955fbf1d6966422be25dd8b4b44f0bd8a5c67bf04800ff88764acd[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "47304402206f75863b212819912e871f9071825d0274b8bdb4d736bad4c86ad95a6f3497a2022066145d851d955fbf1d6966422be25dd8b4b44f0bd8a5c67bf04800ff88764acd012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "010000000151b0972afeb84df66fdb809d0aaeeb0ca4693f6694c9817c114b77b3eadc3d53020000006a47304402206f75863b212819912e871f9071825d0274b8bdb4d736bad4c86ad95a6f3497a2022066145d851d955fbf1d6966422be25dd8b4b44f0bd8a5c67bf04800ff88764acd012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "17ec63913d4798460cf65fe995d6829bc6f634be9f31b7f6a69ca95ea0b333f3", + "confirmations": 588045, + "time": 1520368002, + "blocktime": 1520368002 +} \ No newline at end of file diff --git a/server/test/resources/transactions/a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5 b/server/test/resources/transactions/a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5 new file mode 100644 index 0000000..38635ec --- /dev/null +++ b/server/test/resources/transactions/a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5 @@ -0,0 +1,62 @@ +{ + "txid": "a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5", + "hash": "a7cf3af87f66e58b43de4eb61e3bad6185fd8e3137d63c1960e4df67088f3fe5", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "4a577640c37dd796ae6f8e0538db636983110f01dc42aeefe254bf370b645d1b", + "vout": 2, + "scriptSig": { + "asm": "3045022100838d27b2a574dd99ae90db1703cbcdccdc59edeb98a6421216aefeec1b036e0f02206e5ec511ef654ba8d3df0bfddf513538909f4ff113b9d3c4752804425c39edf5[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "483045022100838d27b2a574dd99ae90db1703cbcdccdc59edeb98a6421216aefeec1b036e0f02206e5ec511ef654ba8d3df0bfddf513538909f4ff113b9d3c4752804425c39edf5012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "01000000011b5d640b37bf54e2efae42dc010f11836963db38058e6fae96d77dc34076574a020000006b483045022100838d27b2a574dd99ae90db1703cbcdccdc59edeb98a6421216aefeec1b036e0f02206e5ec511ef654ba8d3df0bfddf513538909f4ff113b9d3c4752804425c39edf5012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "98513955cc3342bd26c7dfb88c69765ce05ffc1493c0a9beb38c1e02fc11e6c0", + "confirmations": 587836, + "time": 1520380830, + "blocktime": 1520380830 +} \ No newline at end of file diff --git a/server/test/resources/transactions/b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba b/server/test/resources/transactions/b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba new file mode 100644 index 0000000..61e8670 --- /dev/null +++ b/server/test/resources/transactions/b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba @@ -0,0 +1,62 @@ +{ + "txid": "b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba", + "hash": "b20e438c4c54cb048dbcb5f39de17dcaf6418adb521f5ff9d29c13239d50bcba", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "537e1fee3c18dce91cf5b743e0fb9836704b80f57fd320e82e00dcfc38e9b4b4", + "vout": 2, + "scriptSig": { + "asm": "3045022100f95f0737e8e386862b7740e8929a712e7aeab0953c1ec51c28142c81f17d091a022019798947402d08debca1e413428e37dd4f1fcdd703a360ccbc1abccaedecbee4[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "483045022100f95f0737e8e386862b7740e8929a712e7aeab0953c1ec51c28142c81f17d091a022019798947402d08debca1e413428e37dd4f1fcdd703a360ccbc1abccaedecbee4012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "0100000001b4b4e938fcdc002ee820d37ff5804b703698fbe043b7f51ce9dc183cee1f7e53020000006b483045022100f95f0737e8e386862b7740e8929a712e7aeab0953c1ec51c28142c81f17d091a022019798947402d08debca1e413428e37dd4f1fcdd703a360ccbc1abccaedecbee4012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "c1d2b6ceb55487d4d35f90d45d44ae34faf2462a4457e645a3d50ab8bdf74c0a", + "confirmations": 588087, + "time": 1520366239, + "blocktime": 1520366239 +} \ No newline at end of file diff --git a/server/test/resources/transactions/bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c b/server/test/resources/transactions/bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c new file mode 100644 index 0000000..61b4ab0 --- /dev/null +++ b/server/test/resources/transactions/bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c @@ -0,0 +1,62 @@ +{ + "txid": "bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c", + "hash": "bd715a5f43d73a5f347b7779859b029be069cc8ea6fc6480dfda40b64d828c6c", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "40ceeb530b29e5426236d3fcf8cc2162625bdd6a09f61f291452b351a2e411cf", + "vout": 1, + "scriptSig": { + "asm": "3045022100ae6b492bd7fc007ca208ed609b8ad3e617c3d890ef4d24c92d6449f4db2b20810220588a30755b267d23a894af3f47512190c17beb969f145a2ac9a41bff91e93ea2[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "483045022100ae6b492bd7fc007ca208ed609b8ad3e617c3d890ef4d24c92d6449f4db2b20810220588a30755b267d23a894af3f47512190c17beb969f145a2ac9a41bff91e93ea2012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "0100000001cf11e4a251b35214291ff6096add5b626221ccf8fcd3366242e5290b53ebce40010000006b483045022100ae6b492bd7fc007ca208ed609b8ad3e617c3d890ef4d24c92d6449f4db2b20810220588a30755b267d23a894af3f47512190c17beb969f145a2ac9a41bff91e93ea2012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "0bf973bfa84d49f85f6af32e53ccb577e43f725a6996c37ccc377f54ceebb463", + "confirmations": 587655, + "time": 1520391921, + "blocktime": 1520391921 +} \ No newline at end of file diff --git a/server/test/resources/transactions/da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a b/server/test/resources/transactions/da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a new file mode 100644 index 0000000..d89b625 --- /dev/null +++ b/server/test/resources/transactions/da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a @@ -0,0 +1,62 @@ +{ + "txid": "da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a", + "hash": "da1af8037b1cb20f6702eef636d27871ab99bd69031668fafc0ae6cbe4f9941a", + "version": 1, + "size": 235, + "vsize": 235, + "weight": 940, + "locktime": 0, + "vin": [ + { + "txid": "d5fd9995d441cb1fb885f2ad22962c41adfd8c407e423973c3b950210acbf941", + "vout": 2, + "scriptSig": { + "asm": "3045022100e0928e1b40b8882d361412e2ebdc3362652a36a5c5f809b37e43e4fc4e09966902200846c62e14dbbc6c0f10cecc5852d6a4d42247a98287e6359d633e5df9e65f76[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "483045022100e0928e1b40b8882d361412e2ebdc3362652a36a5c5f809b37e43e4fc4e09966902200846c62e14dbbc6c0f10cecc5852d6a4d42247a98287e6359d633e5df9e65f76012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "010000000141f9cb0a2150b9c37339427e408cfdad412c9622adf285b81fcb41d49599fdd5020000006b483045022100e0928e1b40b8882d361412e2ebdc3362652a36a5c5f809b37e43e4fc4e09966902200846c62e14dbbc6c0f10cecc5852d6a4d42247a98287e6359d633e5df9e65f76012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "92435e3280730249ce3fac51e4c8d690c300a1493032ca5482e01f0179050d05", + "confirmations": 586977, + "time": 1520433722, + "blocktime": 1520433722 +} \ No newline at end of file diff --git a/server/test/resources/transactions/e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188 b/server/test/resources/transactions/e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188 new file mode 100644 index 0000000..d0cb764 --- /dev/null +++ b/server/test/resources/transactions/e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188 @@ -0,0 +1,62 @@ +{ + "txid": "e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188", + "hash": "e66cfa37f10fe7e75f2414fb3465dda7864acbdb9885f519217dd147178b7188", + "version": 1, + "size": 234, + "vsize": 234, + "weight": 936, + "locktime": 0, + "vin": [ + { + "txid": "342d9b16e5bdeeeb4ada59de3179dbe40a1563447b3afd0e0451f31819b45ba6", + "vout": 2, + "scriptSig": { + "asm": "30440220686a805f81afd322e3a412410679d139481b513f04a7550f1d44d41f6cb407c6022058881887de78b94f223369df3996b071c0ec49bf6bbaba73ea10326ced3589b7[ALL] 03624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1", + "hex": "4730440220686a805f81afd322e3a412410679d139481b513f04a7550f1d44d41f6cb407c6022058881887de78b94f223369df3996b071c0ec49bf6bbaba73ea10326ced3589b7012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 73242.18642578, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + }, + { + "value": 73242.18642578, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 3cc9ede1da2d7351aaebaf6a25d2657e0b05a716 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" + ] + } + } + ], + "hex": "0100000001a65bb41918f351040efd3a7b4463150ae4db7931de59da4aebeebde5169b2d34020000006a4730440220686a805f81afd322e3a412410679d139481b513f04a7550f1d44d41f6cb407c6022058881887de78b94f223369df3996b071c0ec49bf6bbaba73ea10326ced3589b7012103624fbfb0079e85bbc9aaeba6f48581326ad01194b3c54ce22852a27b1d2892d1ffffffff030000000000000000009250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac9250734da90600001976a9143cc9ede1da2d7351aaebaf6a25d2657e0b05a71688ac00000000", + "blockhash": "2855f642d5e64f2a627eda761d55b9a8ab9e5eccd572655439a27fa429548815", + "confirmations": 587817, + "time": 1520381857, + "blocktime": 1520381857 +} \ No newline at end of file diff --git a/server/test/resources/transactions/f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad b/server/test/resources/transactions/f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad new file mode 100644 index 0000000..7583541 --- /dev/null +++ b/server/test/resources/transactions/f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad @@ -0,0 +1,88 @@ +{ + "txid": "f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad", + "hash": "f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad", + "version": 1, + "size": 196, + "vsize": 196, + "weight": 784, + "locktime": 0, + "vin": [ + { + "txid": "b5dddd087c051aeec5ce59a96adafc5449bb212ecd3892d7ff6456e573d7737b", + "vout": 1, + "scriptSig": { + "asm": "", + "hex": "" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "", + "hex": "", + "type": "nonstandard" + } + }, + { + "value": 2536.19375, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 c9ba3c497107845ea80dcf64951ced030556384e OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914c9ba3c497107845ea80dcf64951ced030556384e88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge" + ] + } + }, + { + "value": 2513.91875, + "n": 2, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 c9ba3c497107845ea80dcf64951ced030556384e OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914c9ba3c497107845ea80dcf64951ced030556384e88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge" + ] + } + }, + { + "value": 0.225, + "n": 3, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 0651c16ab25fa5a6b8c0598b6fd18c493ad25700 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9140651c16ab25fa5a6b8c0598b6fd18c493ad2570088ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XbGFpsuhv6AH3gp3dx5eQrAexP5kESh9bY" + ] + } + }, + { + "value": 22.5, + "n": 4, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 b4b58ae50a9e0a6e5837c1fe80c0502054534124 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914b4b58ae50a9e0a6e5837c1fe80c050205453412488ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "XsALwaebmmPSXViyfacBRhp68f7KWwiNvW" + ] + } + } + ], + "hex": "01000000017b73d773e55664ffd79238cd2e21bb4954fcda6aa959cec5ee1a057c08ddddb50100000000ffffffff05000000000000000000988be40c3b0000001976a914c9ba3c497107845ea80dcf64951ced030556384e88acb8971f883a0000001976a914c9ba3c497107845ea80dcf64951ced030556384e88aca0525701000000001976a9140651c16ab25fa5a6b8c0598b6fd18c493ad2570088ac80461c86000000001976a914b4b58ae50a9e0a6e5837c1fe80c050205453412488ac00000000", + "blockhash": "0ef7374452abd7dc71a9350a16813ba2df3489c626076296824aebf203c20cda", + "confirmations": 556369, + "time": 1522420058, + "blocktime": 1522420058 +} \ No newline at end of file