Browse Source

server: Add support for TPoS blocks

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
a608fd4845
  1. 9
      server/app/com/xsn/explorer/errors/blockErrors.scala
  2. 2
      server/app/com/xsn/explorer/models/blockRewards.scala
  3. 46
      server/app/com/xsn/explorer/services/BlockService.scala
  4. 60
      server/app/com/xsn/explorer/services/logic/BlockLogic.scala
  5. 1
      server/conf/messages
  6. 3
      server/test/com/xsn/explorer/helpers/DataHelper.scala
  7. 109
      server/test/controllers/BlocksControllerSpec.scala

9
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)
}
}

2
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

46
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
}
}

60
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
}
}
}

1
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

3
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)
}
}

109
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

Loading…
Cancel
Save