From a608fd4845b5bb4d1c3e37fb8a0bcc6ed2a5c93d Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 24 Mar 2018 17:02:55 -0600 Subject: [PATCH] server: Add support for TPoS blocks --- .../com/xsn/explorer/errors/blockErrors.scala | 9 -- .../xsn/explorer/models/blockRewards.scala | 2 + .../xsn/explorer/services/BlockService.scala | 46 +++++++- .../explorer/services/logic/BlockLogic.scala | 60 ++++++++++ server/conf/messages | 1 - .../com/xsn/explorer/helpers/DataHelper.scala | 3 + .../controllers/BlocksControllerSpec.scala | 109 +++++++++++++++--- 7 files changed, 198 insertions(+), 32 deletions(-) diff --git a/server/app/com/xsn/explorer/errors/blockErrors.scala b/server/app/com/xsn/explorer/errors/blockErrors.scala index fb1f4e3..38768af 100644 --- a/server/app/com/xsn/explorer/errors/blockErrors.scala +++ b/server/app/com/xsn/explorer/errors/blockErrors.scala @@ -22,12 +22,3 @@ case object BlockNotFoundError extends BlockError with InputValidationError { List(error) } } - -case object TPoSBlockNotSupportedError extends BlockError with InputValidationError { - - override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { - val message = messagesApi("error.block.tposUnsupported") - val error = FieldValidationError("blockhash", message) - List(error) - } -} diff --git a/server/app/com/xsn/explorer/models/blockRewards.scala b/server/app/com/xsn/explorer/models/blockRewards.scala index 585f887..5f39f72 100644 --- a/server/app/com/xsn/explorer/models/blockRewards.scala +++ b/server/app/com/xsn/explorer/models/blockRewards.scala @@ -8,8 +8,10 @@ object BlockRewards { implicit val writes: Writes[BlockRewards] = Writes[BlockRewards] { case r: PoWBlockRewards => Json.writes[PoWBlockRewards].writes(r) case r: PoSBlockRewards => Json.writes[PoSBlockRewards].writes(r) + case r: TPoSBlockRewards => Json.writes[TPoSBlockRewards].writes(r) } } case class PoWBlockRewards(reward: BlockReward) extends BlockRewards case class PoSBlockRewards(coinstake: BlockReward, masternode: Option[BlockReward]) extends BlockRewards +case class TPoSBlockRewards(owner: BlockReward, merchant: BlockReward, masternode: Option[BlockReward]) extends BlockRewards diff --git a/server/app/com/xsn/explorer/services/BlockService.scala b/server/app/com/xsn/explorer/services/BlockService.scala index 2a578bd..c44580a 100644 --- a/server/app/com/xsn/explorer/services/BlockService.scala +++ b/server/app/com/xsn/explorer/services/BlockService.scala @@ -4,12 +4,11 @@ import javax.inject.Inject import com.alexitc.playsonify.core.FutureApplicationResult import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} -import com.xsn.explorer.errors.{BlockNotFoundError, TPoSBlockNotSupportedError} +import com.xsn.explorer.errors.BlockNotFoundError import com.xsn.explorer.models._ import com.xsn.explorer.services.logic.{BlockLogic, TransactionLogic} -import org.scalactic.Bad -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext class BlockService @Inject() ( xsnService: XSNService, @@ -85,9 +84,44 @@ class BlockService @Inject() ( result.toFuture } - // TODO: Complete it + // TODO: Handle blocks with coin split private def getTPoSBlockRewards(block: Block): FutureApplicationResult[BlockRewards] = { - val result = Bad(TPoSBlockNotSupportedError).accumulating - Future.successful(result) + val result = for { + coinstakeTxId <- blockLogic + .getCoinstakeTransactionId(block) + .toFutureOr + coinstakeTx <- xsnService + .getTransaction(coinstakeTxId) + .toFutureOr + coinstakeTxVIN <- transactionLogic + .getVIN(coinstakeTx, BlockNotFoundError) + .toFutureOr + + previousToCoinstakeTx <- xsnService + .getTransaction(coinstakeTxVIN.txid) + .toFutureOr + previousToCoinstakeVOUT <- transactionLogic + .getVOUT(coinstakeTxVIN, previousToCoinstakeTx, BlockNotFoundError) + .toFutureOr + + tposTxId <- blockLogic + .getTPoSTransactionId(block) + .toFutureOr + tposTx <- xsnService + .getTransaction(tposTxId) + .toFutureOr + + addresses <- blockLogic + .getTPoSAddresses(tposTx) + .toFutureOr + + (ownerAddress, merchantAddress) = addresses + + rewards <- blockLogic + .getTPoSRewards(coinstakeTx, ownerAddress, merchantAddress, previousToCoinstakeVOUT.value) + .toFutureOr + } yield rewards + + 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 b649411..bc5c5ab 100644 --- a/server/app/com/xsn/explorer/services/logic/BlockLogic.scala +++ b/server/app/com/xsn/explorer/services/logic/BlockLogic.scala @@ -31,6 +31,22 @@ class BlockLogic { Or.from(maybe, One(BlockNotFoundError)) } + def getTPoSTransactionId(block: Block): ApplicationResult[TransactionId] = { + val maybe = block.tposContract + + Or.from(maybe, One(BlockNotFoundError)) + } + + def getTPoSAddresses(tposContract: Transaction): ApplicationResult[(Address, Address)] = { + val maybe = tposContract + .vout + .flatMap(_.scriptPubKey) + .flatMap(_.getTPoSAddresses) + .headOption + + Or.from(maybe, One(BlockNotFoundError)) + } + /** * Computes the rewards for a PoS coinstake transaction. * @@ -80,4 +96,48 @@ class BlockLogic { Bad(BlockNotFoundError).accumulating } } + + // TODO: Complete it for coin split + def getTPoSRewards( + coinstakeTx: Transaction, + owner: Address, + merchant: Address, + coinstakeInput: BigDecimal): ApplicationResult[TPoSBlockRewards] = { + + // first vout is empty, useless + val coinstakeVOUT = coinstakeTx.vout.drop(1) + + // TODO: We can probably have upto 4 outputs + if (coinstakeVOUT.size >= 1 && coinstakeVOUT.size <= 3) { + val ownerValue = coinstakeVOUT + .filter(_.address contains owner) + .map(_.value) + .sum + + val ownerReward = BlockReward( + owner, + (ownerValue - coinstakeInput) max 0) + + // merchant + val merchantValue = coinstakeVOUT.filter(_.address contains merchant).map(_.value).sum + val merchantReward = BlockReward(merchant, merchantValue) + + // master node + val masternodeRewardOUT = coinstakeVOUT.filterNot { out => + out.address.contains(owner) || + out.address.contains(merchant) + } + val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.address).headOption + val masternodeRewardMaybe = masternodeAddressMaybe.map { masternodeAddress => + BlockReward( + masternodeAddress, + masternodeRewardOUT.filter(_.address contains masternodeAddress).map(_.value).sum + ) + } + + Good(TPoSBlockRewards(ownerReward, merchantReward, masternodeRewardMaybe)) + } else { + Bad(BlockNotFoundError).accumulating + } + } } diff --git a/server/conf/messages b/server/conf/messages index 740ac5f..873e2d0 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -9,4 +9,3 @@ error.address.format=Invalid address format error.block.format=Invalid blockhash format error.block.notFound=Block not found -error.block.tposUnsupported=TPoS Blocks are not supported at the moment diff --git a/server/test/com/xsn/explorer/helpers/DataHelper.scala b/server/test/com/xsn/explorer/helpers/DataHelper.scala index bb2a542..02e652a 100644 --- a/server/test/com/xsn/explorer/helpers/DataHelper.scala +++ b/server/test/com/xsn/explorer/helpers/DataHelper.scala @@ -20,4 +20,7 @@ object DataHelper { ScriptPubKey(scriptType, "", List(address)) } + def createScriptPubKey(scriptType: String, asm: String, address: Option[Address] = None) = { + ScriptPubKey(scriptType, asm, address.toList) + } } diff --git a/server/test/controllers/BlocksControllerSpec.scala b/server/test/controllers/BlocksControllerSpec.scala index bf5bff4..c7722e3 100644 --- a/server/test/controllers/BlocksControllerSpec.scala +++ b/server/test/controllers/BlocksControllerSpec.scala @@ -79,8 +79,58 @@ class BlocksControllerSpec extends MyAPISpec { // TPoS val tposBlock = posBlock.copy( - hash = Blockhash.from("c6944a33e3e03eb0ccd350f1fc2d6e5f3bd1411e1efddc0990aa3243663b41b7").get, - tposContract = Some(createTransactionId("7f2b5f25b0ae24a417633e4214827f930a69802c1c43d1fb2ff7b7075b2d1701"))) + hash = Blockhash.from("19f320185015d146237efe757852b21c5e08b88b2f4de9d3fa9517d8463e472b").get, + tposContract = Some(createTransactionId("7f2b5f25b0ae24a417633e4214827f930a69802c1c43d1fb2ff7b7075b2d1701")), + transactions = List( + createTransactionId("28568eb4a2c69a292b7d56daa45e3b17fbfc8af9310d5c2d444600e64266c87f"), + createTransactionId("8c7feafc18576b89bf87faf8aa89feaac1a3fad7d5da77d1fe773219a0e9d864") + ) + ) + + val tposBlockContractTx = createTx( + createTransactionId("7f2b5f25b0ae24a417633e4214827f930a69802c1c43d1fb2ff7b7075b2d1701"), + None, + List( + TransactionVOUT( + n = 0, + value = BigDecimal(0), + scriptPubKey = Some(createScriptPubKey("nulldata", "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99")) + ) + ) + ) + + val tposBlockCoinstakeTx = createTx( + createTransactionId("8c7feafc18576b89bf87faf8aa89feaac1a3fad7d5da77d1fe773219a0e9d864"), + Some(TransactionVIN(createTransactionId("9ecf10916467dccc8c8f3a87d869dc5aceb57d5d1c2117036fe60f31369a284e"), 1)), + List( + TransactionVOUT(BigDecimal(0), 0, None), + TransactionVOUT( + n = 1, + value = BigDecimal("1022.27500000"), + scriptPubKey = Some(createScriptPubKey("pubkeyhash", createAddress("Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw"))) + ), + + TransactionVOUT( + n = 2, + value = BigDecimal("0.22500000"), + scriptPubKey = Some(createScriptPubKey("pubkeyhash", createAddress("XyJC8xnfFrHNcMinh6gxuPRYY9HCaY9DAo"))) + ), + + TransactionVOUT( + n = 3, + value = BigDecimal("22.50000000"), + scriptPubKey = Some(createScriptPubKey("pubkeyhash", createAddress("XydZnssXHCxxRtB4rk7evfKT9XP7GqyA9N"))) + ) + ) + ) + + val tposBlockCoinstakeTxInput = createTx( + createTransactionId("9ecf10916467dccc8c8f3a87d869dc5aceb57d5d1c2117036fe60f31369a284e"), + None, + List( + TransactionVOUT(n = 1, value = BigDecimal(1000)) + ) + ) // PoW val powBlock = posBlock.copy( @@ -122,7 +172,10 @@ class BlocksControllerSpec extends MyAPISpec { posBlockCoinstakeTxInput.id -> posBlockCoinstakeTxInput, posBlockRoundingErrorCoinstakeTx.id -> posBlockRoundingErrorCoinstakeTx, posBlockRoundingErrorCoinstakeTxInput.id -> posBlockRoundingErrorCoinstakeTxInput, - powBlockPreviousTx.id -> powBlockPreviousTx + powBlockPreviousTx.id -> powBlockPreviousTx, + tposBlockContractTx.id -> tposBlockContractTx, + tposBlockCoinstakeTx.id -> tposBlockCoinstakeTx, + tposBlockCoinstakeTxInput.id -> tposBlockCoinstakeTxInput ) override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { @@ -240,24 +293,48 @@ class BlocksControllerSpec extends MyAPISpec { (jsonReward \ "value").as[BigDecimal] mustEqual BigDecimal("76500000") } - "fail on the wrong blockhash format" in { - val response = GET(url("000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5")) + "retrieve TPoS block" in { + val block = tposBlock + val response = GET(url("19f320185015d146237efe757852b21c5e08b88b2f4de9d3fa9517d8463e472b")) - status(response) mustEqual BAD_REQUEST + status(response) mustEqual OK val json = contentAsJson(response) - val errorList = (json \ "errors").as[List[JsValue]] + val jsonBlock = (json \ "block").as[JsValue] + val jsonRewards = (json \ "rewards").as[JsValue] - errorList.size mustEqual 1 - val error = errorList.head + (jsonBlock \ "hash").as[Blockhash] mustEqual block.hash + (jsonBlock \ "size").as[Size] mustEqual block.size + (jsonBlock \ "bits").as[String] mustEqual block.bits + (jsonBlock \ "chainwork").as[String] mustEqual block.chainwork + (jsonBlock \ "difficulty").as[BigDecimal] mustEqual block.difficulty + (jsonBlock \ "confirmations").as[Confirmations] mustEqual block.confirmations + (jsonBlock \ "height").as[Height] mustEqual block.height + (jsonBlock \ "medianTime").as[Long] mustEqual block.medianTime + (jsonBlock \ "time").as[Long] mustEqual block.time + (jsonBlock \ "merkleRoot").as[Blockhash] mustEqual block.merkleRoot + (jsonBlock \ "version").as[Long] mustEqual block.version + (jsonBlock \ "nonce").as[Int] mustEqual block.nonce + (jsonBlock \ "previousBlockhash").asOpt[Blockhash] mustEqual block.previousBlockhash + (jsonBlock \ "nextBlockhash").asOpt[Blockhash] mustEqual block.nextBlockhash + (jsonBlock \ "tposContract").as[String] mustEqual block.tposContract.get.string + + val jsonOwner = (jsonRewards \ "owner").as[JsValue] + (jsonOwner \ "address").as[String] mustEqual "Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw" + (jsonOwner \ "value").as[BigDecimal] mustEqual BigDecimal("22.275") + + val jsonMerchant = (jsonRewards \ "merchant").as[JsValue] + (jsonMerchant \ "address").as[String] mustEqual "XyJC8xnfFrHNcMinh6gxuPRYY9HCaY9DAo" + (jsonMerchant \ "value").as[BigDecimal] mustEqual BigDecimal("0.225") + + val jsonMasternode = (jsonRewards \ "masternode").as[JsValue] + (jsonMasternode \ "address").as[String] mustEqual "XydZnssXHCxxRtB4rk7evfKT9XP7GqyA9N" + (jsonMasternode \ "value").as[BigDecimal] mustEqual BigDecimal("22.5") - (error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType - (error \ "field").as[String] mustEqual "blockhash" - (error \ "message").as[String].nonEmpty mustEqual true } - "fail on an unknown block" in { - val response = GET(url("000003dc4c2fc449dededaaad6efc33ce1b64b88a060652dc47edc63d6d6b524")) + "fail on the wrong blockhash format" in { + val response = GET(url("000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5")) status(response) mustEqual BAD_REQUEST @@ -272,8 +349,8 @@ class BlocksControllerSpec extends MyAPISpec { (error \ "message").as[String].nonEmpty mustEqual true } - "fail on TPoS block" in { - val response = GET(url("c6944a33e3e03eb0ccd350f1fc2d6e5f3bd1411e1efddc0990aa3243663b41b7")) + "fail on an unknown block" in { + val response = GET(url("000003dc4c2fc449dededaaad6efc33ce1b64b88a060652dc47edc63d6d6b524")) status(response) mustEqual BAD_REQUEST