Browse Source

server: Update the database schema to support utxos (#35)

prometheus-integration
Alexis Hernandez 7 years ago
parent
commit
dee0f7e600
  1. 7
      server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala
  2. 30
      server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala
  3. 22
      server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala
  4. 21
      server/app/com/xsn/explorer/models/Transaction.scala
  5. 14
      server/app/com/xsn/explorer/models/rpc/ScriptPubKey.scala
  6. 2
      server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala
  7. 4
      server/app/com/xsn/explorer/util/Extensions.scala
  8. 16
      server/conf/evolutions/default/4.sql
  9. 42
      server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala
  10. 16
      server/test/com/xsn/explorer/helpers/DataHelper.scala
  11. 16
      server/test/com/xsn/explorer/models/rpc/ScriptPubKeySpec.scala
  12. 17
      server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala
  13. 24
      server/test/com/xsn/explorer/util/BigDecimalExtSpec.scala

7
server/app/com/xsn/explorer/data/anorm/LedgerPostgresDataHandler.scala

@ -108,12 +108,7 @@ class LedgerPostgresDataHandler @Inject() (
transactions
.map(_.inputs)
.flatMap { inputs =>
inputs.flatMap { input =>
for {
address <- input.address
value <- input.value
} yield address -> value
}
inputs.map { input => input.address -> input.value }
}
.groupBy(_._1)
.mapValues { list => list.map(_._2).sum }

30
server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala

@ -188,20 +188,24 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi
SQL(
"""
|INSERT INTO transaction_inputs
| (txid, index, value, address)
| (txid, index, from_txid, from_output_index, value, address)
|VALUES
| ({txid}, {index}, {value}, {address})
| ({txid}, {index}, {from_txid}, {from_output_index}, {value}, {address})
|ON CONFLICT (txid, index) DO UPDATE
| SET value = EXCLUDED.value,
| address = EXCLUDED.address
|RETURNING index, value, address
| address = EXCLUDED.address,
| from_txid = EXCLUDED.from_txid,
| from_output_index = EXCLUDED.from_output_index
|RETURNING txid, index, from_txid, from_output_index, value, address
""".stripMargin
).on(
'txid -> transactionId.string,
'index -> input.index,
'from_txid -> input.fromTxid.string,
'from_output_index -> input.fromOutputIndex,
'value -> input.value,
'address -> input.address.map(_.string)
).as(parseTransactionInput.singleOpt)
'address -> input.address.string
).as(parseTransactionInput.singleOpt).flatten
}
private def upsertOutputs(
@ -228,21 +232,23 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi
SQL(
"""
|INSERT INTO transaction_outputs
| (txid, index, value, address, tpos_owner_address, tpos_merchant_address)
| (txid, index, value, address, hex_script, tpos_owner_address, tpos_merchant_address)
|VALUES
| ({txid}, {index}, {value}, {address}, {tpos_owner_address}, {tpos_merchant_address})
| ({txid}, {index}, {value}, {address}, {hex_script}, {tpos_owner_address}, {tpos_merchant_address})
|ON CONFLICT (txid, index) DO UPDATE
| SET value = EXCLUDED.value,
| address = EXCLUDED.address,
| hex_script = EXCLUDED.hex_script,
| tpos_owner_address = EXCLUDED.tpos_owner_address,
| tpos_merchant_address = EXCLUDED.tpos_merchant_address
|RETURNING index, value, address, tpos_owner_address, tpos_merchant_address
|RETURNING txid, index, value, address, hex_script, tpos_owner_address, tpos_merchant_address
""".stripMargin
).on(
'txid -> transactionId.string,
'index -> output.index,
'value -> output.value,
'address -> output.address.string,
'hex_script -> output.script.string,
'tpos_owner_address -> output.tposOwnerAddress.map(_.string),
'tpos_merchant_address -> output.tposMerchantAddress.map(_.string)
).as(parseTransactionOutput.singleOpt).flatten
@ -253,11 +259,11 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi
"""
|DELETE FROM transaction_inputs
|WHERE txid = {txid}
|RETURNING index, value, address
|RETURNING txid, index, from_txid, from_output_index, value, address
""".stripMargin
).on(
'txid -> txid.string
).as(parseTransactionInput.*)
).as(parseTransactionInput.*).flatten
}
private def deleteOutputs(txid: TransactionId)(implicit conn: Connection): List[Transaction.Output] = {
@ -265,7 +271,7 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi
"""
|DELETE FROM transaction_outputs
|WHERE txid = {txid}
|RETURNING index, value, address, tpos_owner_address, tpos_merchant_address
|RETURNING txid, index, hex_script, value, address, tpos_owner_address, tpos_merchant_address
""".stripMargin
).on(
'txid -> txid.string

22
server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala

@ -2,19 +2,22 @@ package com.xsn.explorer.data.anorm.parsers
import anorm.SqlParser.{get, str}
import anorm.~
import com.xsn.explorer.models.{Address, Transaction, TransactionId, TransactionWithValues}
import com.xsn.explorer.models._
object TransactionParsers {
import CommonParsers._
val parseTransactionId = str("txid").map(TransactionId.from)
val parseFromTxid = str("from_txid").map(TransactionId.from)
val parseFromOutputIndex = get[Int]("from_output_index")
val parseReceived = get[BigDecimal]("received")
val parseSpent = get[BigDecimal]("spent")
val parseSent = get[BigDecimal]("sent")
val parseIndex = get[Int]("index")
val parseValue = get[BigDecimal]("value")
val parseHexString = get[String]("hex_script").map(HexString.from)
val parseTposOwnerAddress = str("tpos_owner_address").map(Address.from)
val parseTposMerchantAddress = str("tpos_merchant_address").map(Address.from)
@ -42,19 +45,24 @@ object TransactionParsers {
} yield TransactionWithValues(txid, blockhash, time, size, sent, received)
}
val parseTransactionInput = (parseIndex ~ parseValue.? ~ parseAddress.?).map { case index ~ value ~ address =>
Transaction.Input(index, value, address.flatten)
}
val parseTransactionInput = (parseFromTxid ~ parseFromOutputIndex ~ parseIndex ~ parseValue ~ parseAddress)
.map { case fromTxidMaybe ~ fromOutputIndex ~ index ~ value ~ addressMaybe =>
for {
from <- fromTxidMaybe
address <- addressMaybe
} yield Transaction.Input(from, fromOutputIndex, index, value, address)
}
val parseTransactionOutput = (
parseIndex ~
parseValue ~
parseAddress ~
parseHexString ~
parseTposOwnerAddress.? ~
parseTposMerchantAddress.?).map {
case index ~ value ~ addressMaybe ~ tposOwnerAddress ~ tposMerchantAddress =>
for (address <- addressMaybe)
yield Transaction.Output(index, value, address, tposOwnerAddress.flatten, tposMerchantAddress.flatten)
case index ~ value ~ addressMaybe ~ scriptMaybe ~ tposOwnerAddress ~ tposMerchantAddress =>
for (address <- addressMaybe; script <- scriptMaybe)
yield Transaction.Output(index, value, address, script, tposOwnerAddress.flatten, tposMerchantAddress.flatten)
}
}

21
server/app/com/xsn/explorer/models/Transaction.scala

@ -10,15 +10,21 @@ case class Transaction(
object Transaction {
/**
* The coins where generated on the given output index of the given txid (from).
*/
case class Input(
fromTxid: TransactionId,
fromOutputIndex: Int,
index: Int,
value: Option[BigDecimal],
address: Option[Address])
value: BigDecimal,
address: Address)
case class Output(
index: Int,
value: BigDecimal,
address: Address,
script: HexString,
tposOwnerAddress: Option[Address],
tposMerchantAddress: Option[Address])
@ -29,15 +35,20 @@ object Transaction {
* the utxo index or the getTransaction method from the TransactionService..
*/
def fromRPC(tx: rpc.Transaction): Transaction = {
val inputs = tx.vin.zipWithIndex.map { case (vin, index) =>
Transaction.Input(index, vin.value, vin.address)
val inputs = tx.vin.zipWithIndex.flatMap { case (vin, index) =>
for {
value <- vin.value
address <- vin.address
} yield Transaction.Input(vin.txid, vin.voutIndex, index, value, address)
}
val outputs = tx.vout.flatMap { vout =>
val tposAddresses = vout.scriptPubKey.flatMap(_.getTPoSAddresses)
val scriptMaybe = vout.scriptPubKey.map(_.hex)
for {
address <- vout.address
} yield Transaction.Output(vout.n, vout.value, address, tposAddresses.map(_._1), tposAddresses.map(_._2))
script <- scriptMaybe
} yield Transaction.Output(vout.n, vout.value, address, script, tposAddresses.map(_._1), tposAddresses.map(_._2))
}
Transaction(

14
server/app/com/xsn/explorer/models/rpc/ScriptPubKey.scala

@ -1,12 +1,13 @@
package com.xsn.explorer.models.rpc
import com.xsn.explorer.models.Address
import com.xsn.explorer.models.{Address, HexString}
import play.api.libs.functional.syntax._
import play.api.libs.json.{Reads, __}
case class ScriptPubKey(
`type`: String,
asm: String,
hex: HexString,
addresses: List[Address]) {
/**
@ -27,7 +28,7 @@ case class ScriptPubKey(
.filter(_.size >= 4) // relax size check
.map(_.toList)
.flatMap {
case op :: owner :: merchant :: commission if op == "OP_RETURN" =>
case op :: owner :: merchant :: _ if op == "OP_RETURN" =>
for {
ownerAddress <- Address.fromHex(owner)
merchantAddress <- Address.fromHex(merchant)
@ -40,13 +41,16 @@ case class ScriptPubKey(
object ScriptPubKey {
implicit val reads: Reads[ScriptPubKey] = {
implicit val reads: Reads[Option[ScriptPubKey]] = {
val builder = (__ \ 'type).read[String] and
(__ \ 'asm).read[String] and
(__ \ 'hex).read[String].map(HexString.from) and
(__ \ 'addresses).readNullable[List[Address]].map(_ getOrElse List.empty)
builder.apply { (t, asm, addresses) =>
ScriptPubKey(t, asm, addresses)
builder.apply { (t, asm, hexString, addresses) =>
for {
hex <- hexString
} yield ScriptPubKey(t, asm = asm, hex = hex, addresses = addresses)
}
}
}

2
server/app/com/xsn/explorer/models/rpc/TransactionVOUT.scala

@ -17,7 +17,7 @@ object TransactionVOUT {
implicit val reads: Reads[TransactionVOUT] = {
val builder = (__ \ 'value).read[BigDecimal] and
(__ \ 'n).read[Int] and
(__ \ 'scriptPubKey).readNullable[ScriptPubKey]
(__ \ 'scriptPubKey).read[Option[ScriptPubKey]]
builder.apply { (value, n, script) =>
TransactionVOUT(value, n, script)

4
server/app/com/xsn/explorer/util/Extensions.scala

@ -14,6 +14,10 @@ object Extensions {
def fromSatoshis: BigDecimal = {
inner / SatoshiScale
}
def toSatoshis: BigInt = {
(inner * SatoshiScale).toBigInt()
}
}
implicit class ListOptionExt[+A](val inner: List[Option[A]]) extends AnyVal {

16
server/conf/evolutions/default/4.sql

@ -14,15 +14,18 @@ CREATE TABLE transactions(
CREATE INDEX transactions_blockhash_index ON transactions USING BTREE (blockhash);
CREATE INDEX transactions_time_index ON transactions USING BTREE (time);
-- TODO: it might be worth to add a unique constraint based for (from_txid, from_output_index)
CREATE TABLE transaction_inputs(
txid TXID_TYPE NOT NULL,
index NON_NEGATIVE_INT_TYPE NOT NULL,
from_txid TXID_TYPE NOT NULL,
from_output_index NON_NEGATIVE_INT_TYPE NOT NULL,
value DECIMAL(30, 15) NULL,
address ADDRESS_TYPE NULL,
-- constraints
CONSTRAINT transaction_inputs_txid_index_pk PRIMARY KEY (txid, index),
CONSTRAINT transaction_inputs_txid_fk FOREIGN KEY (txid) REFERENCES transactions (txid)
CONSTRAINT transaction_inputs_txid_fk FOREIGN KEY (txid) REFERENCES transactions (txid),
CONSTRAINT transaction_inputs_from_txid_fk FOREIGN KEY (from_txid) REFERENCES transactions (txid)
);
CREATE INDEX transaction_inputs_address_index ON transaction_inputs USING BTREE (address);
@ -32,16 +35,19 @@ CREATE TABLE transaction_outputs(
txid TXID_TYPE NOT NULL,
index NON_NEGATIVE_INT_TYPE NOT NULL,
value DECIMAL(30, 15) NOT NULL,
address ADDRESS_TYPE NULL,
address ADDRESS_TYPE NOT NULL,
hex_script TEXT NOT NULL,
spent_on TXID_TYPE NULL,
tpos_owner_address ADDRESS_TYPE NULL,
tpos_merchant_address ADDRESS_TYPE NULL,
-- constraints
CONSTRAINT transaction_outputs_txid_index_pk PRIMARY KEY (txid, index),
CONSTRAINT transaction_outputs_txid_fk FOREIGN KEY (txid) REFERENCES transactions (txid)
CONSTRAINT transaction_outputs_txid_fk FOREIGN KEY (txid) REFERENCES transactions (txid),
CONSTRAINT transaction_outputs_spent_on_fk FOREIGN KEY (spent_on) REFERENCES transactions (txid) ON DELETE SET NULL
);
CREATE INDEX transaction_outputs_address_index ON transaction_outputs USING BTREE (address);
CREATE INDEX transaction_outputs_spent_on ON transaction_outputs USING BTREE (spent_on) WHERE spent_on IS NULL;
# --- !Downs

42
server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala

@ -38,17 +38,26 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be
tposContract = None
)
val dummyTransaction = Transaction(
createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"),
block.hash,
12312312L,
Size(1000),
List.empty,
List.empty
)
val inputs = List(
Transaction.Input(0, None, None),
Transaction.Input(1, Some(BigDecimal(100)), Some(createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F")))
Transaction.Input(dummyTransaction.id, 0, 1, BigDecimal(100), createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F"))
)
val outputs = List(
Transaction.Output(0, BigDecimal(50), createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F"), None, None),
Transaction.Output(0, BigDecimal(50), createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F"), HexString.from("00").get, None, None),
Transaction.Output(
1,
BigDecimal(150),
createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"),
HexString.from("00").get,
Some(createAddress("XfAATXtkRgCdMTrj2fxHvLsKLLmqAjhEAt")),
Some(createAddress("XjfNeGJhLgW3egmsZqdbpCNGfysPs7jTNm")))
)
@ -61,7 +70,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be
inputs,
outputs)
before {
private def prepareBlock(block: Block) = {
val dao = new BlockPostgresDAO
try {
database.withConnection { implicit conn =>
@ -73,6 +82,20 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be
}
}
private def prepareTransaction(transaction: Transaction) = {
try {
dataHandler.upsert(transaction)
} catch {
case _ => ()
}
}
before {
clearDatabase()
prepareBlock(block)
prepareTransaction(dummyTransaction)
}
"upsert" should {
"add a new transaction" in {
val result = dataHandler.upsert(transaction)
@ -109,26 +132,25 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be
dataHandler.upsert(transaction).isGood mustEqual true
val result = dataHandler.deleteBy(transaction.blockhash)
println(result)
result.isGood mustEqual true
result.get mustEqual List(transaction)
result.get.contains(transaction) mustEqual true
}
}
"getBy address" should {
val address = createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW86F")
val inputs = List(
Transaction.Input(0, None, None),
Transaction.Input(1, Some(BigDecimal(100)), Some(address)),
Transaction.Input(2, Some(BigDecimal(200)), Some(createAddress("XxQ7j37LfuXgsLD5DZAwFKhT3s2ZMkW86F")))
Transaction.Input(dummyTransaction.id, 0, 1, 100, address),
Transaction.Input(dummyTransaction.id, 0, 2, 200, createAddress("XxQ7j37LfuXgsLD5DZAwFKhT3s2ZMkW86F"))
)
val outputs = List(
Transaction.Output(0, BigDecimal(50), address, None, None),
Transaction.Output(0, BigDecimal(50), address, HexString.from("00").get, None, None),
Transaction.Output(
1,
BigDecimal(250),
createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"),
HexString.from("00").get,
None, None)
)

16
server/test/com/xsn/explorer/helpers/DataHelper.scala

@ -1,6 +1,5 @@
package com.xsn.explorer.helpers
import com.xsn.explorer.models.rpc.{ScriptPubKey, TransactionVOUT}
import com.xsn.explorer.models.{Address, Blockhash, TransactionId}
object DataHelper {
@ -10,19 +9,4 @@ object DataHelper {
def createBlockhash(string: String) = Blockhash.from(string).get
def createTransactionId(string: String) = TransactionId.from(string).get
def createTransactionVOUT(n: Int, value: BigDecimal, scriptPubKey: ScriptPubKey) = {
TransactionVOUT(
n = n,
value = value,
scriptPubKey = Some(scriptPubKey))
}
def createScriptPubKey(scriptType: String, address: Address) = {
ScriptPubKey(scriptType, "", List(address))
}
def createScriptPubKey(scriptType: String, asm: String, address: Option[Address] = None) = {
ScriptPubKey(scriptType, asm, address.toList)
}
}

16
server/test/com/xsn/explorer/models/rpc/ScriptPubKeySpec.scala

@ -1,14 +1,16 @@
package com.xsn.explorer.models.rpc
import com.xsn.explorer.models.Address
import com.xsn.explorer.models.{Address, HexString}
import org.scalatest.{MustMatchers, OptionValues, WordSpec}
class ScriptPubKeySpec extends WordSpec with MustMatchers with OptionValues {
private val dummyScript = HexString.from("00").get
"getTPoSAddresses" should {
"parse the addresses" in {
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", List.empty)
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", dummyScript, List.empty)
val expected = (
Address.from("Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw").get,
Address.from("XyJC8xnfFrHNcMinh6gxuPRYY9HCaY9DAo").get)
@ -18,35 +20,35 @@ class ScriptPubKeySpec extends WordSpec with MustMatchers with OptionValues {
}
"support more than 4 values if we have the addresses" in {
val script = ScriptPubKey("nulldata", "OP_RETURN 586a55587938507a55464d78534c37594135767866574a587365746b354d5638676f 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99 1f60a6a385a4e5163ffef65dd873f17452bb0d9f89da701ffcc5a0f72287273c0571485c29123fef880d2d8169cfdb884bf95a18a0b36461517acda390ce4cf441", List.empty)
val script = ScriptPubKey("nulldata", "OP_RETURN 586a55587938507a55464d78534c37594135767866574a587365746b354d5638676f 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99 1f60a6a385a4e5163ffef65dd873f17452bb0d9f89da701ffcc5a0f72287273c0571485c29123fef880d2d8169cfdb884bf95a18a0b36461517acda390ce4cf441", dummyScript, List.empty)
val result = script.getTPoSAddresses
result.nonEmpty mustEqual true
}
"fail if OP_RETURN is not present" in {
val script = ScriptPubKey("nulldata", "OP_RTURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", List.empty)
val script = ScriptPubKey("nulldata", "OP_RTURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", dummyScript, List.empty)
val result = script.getTPoSAddresses
result.isEmpty mustEqual true
}
"fail if the comission is missing" in {
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f ", List.empty)
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f ", dummyScript, List.empty)
val result = script.getTPoSAddresses
result.isEmpty mustEqual true
}
"fail if the owner address is malformed" in {
val script = ScriptPubKey("nulldata", "OP_RETURN 586933735164d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", List.empty)
val script = ScriptPubKey("nulldata", "OP_RETURN 586933735164d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99", dummyScript, List.empty)
val result = script.getTPoSAddresses
result.isEmpty mustEqual true
}
"fail if the merchant address is malformed" in {
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786664672484e634d696e68366778755052595939484361593944416f 99", List.empty)
val script = ScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786664672484e634d696e68366778755052595939484361593944416f 99", dummyScript, List.empty)
val result = script.getTPoSAddresses
result.isEmpty mustEqual true

17
server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala

@ -4,7 +4,7 @@ import com.alexitc.playsonify.core.FutureApplicationResult
import com.alexitc.playsonify.validators.PaginatedQueryValidator
import com.xsn.explorer.data.anorm.dao.{BalancePostgresDAO, BlockPostgresDAO, TransactionPostgresDAO}
import com.xsn.explorer.data.anorm.interpreters.FieldOrderingSQLInterpreter
import com.xsn.explorer.data.anorm.{BalancePostgresDataHandler, BlockPostgresDataHandler, LedgerPostgresDataHandler, TransactionPostgresDataHandler}
import com.xsn.explorer.data.anorm.{BlockPostgresDataHandler, LedgerPostgresDataHandler, TransactionPostgresDataHandler}
import com.xsn.explorer.data.async.{BlockFutureDataHandler, LedgerFutureDataHandler, TransactionFutureDataHandler}
import com.xsn.explorer.data.common.PostgresDataHandlerSpec
import com.xsn.explorer.errors.BlockNotFoundError
@ -169,21 +169,6 @@ class LedgerSynchronizerServiceSpec extends PostgresDataHandlerSpec with BeforeA
verifyLedger(finalBlocks: _*)
}
}
"process a block without spent index on transactions" in {
val block = BlockLoader.get("000001ff95f22b8d82db14a5c5e9f725e8239e548be43c668766e7ddaee81924")
.copy(previousBlockhash = None, height = Height(0))
val synchronizer = ledgerSynchronizerService(block)
whenReady(synchronizer.synchronize(block.hash)) { result =>
result.isGood mustEqual true
val balanceDataHandler = new BalancePostgresDataHandler(database, new BalancePostgresDAO(new FieldOrderingSQLInterpreter))
val balance = balanceDataHandler.getBy(DataHelper.createAddress("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"))
balance.get.spent mustEqual BigDecimal("76500000.000000000000000")
}
}
}
private def verifyLedger(blocks: Block*) = {

24
server/test/com/xsn/explorer/util/BigDecimalExtSpec.scala

@ -0,0 +1,24 @@
package com.xsn.explorer.util
import org.scalatest.{MustMatchers, WordSpec}
class BigDecimalExtSpec extends WordSpec with MustMatchers {
import Extensions.BigDecimalExt
"fromSatoshis" should {
"work" in {
val input = BigDecimal(40409891838L)
val result = input.fromSatoshis
result mustEqual BigDecimal(404.09891838)
}
}
"toSatoshis" should {
"work" in {
val input = BigDecimal(404.09891838)
val result = input.toSatoshis
result mustEqual BigDecimal(40409891838L)
}
}
}
Loading…
Cancel
Save