Browse Source

server: Add support for TPoS blocks with coinsplit

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
1972ae8ae8
  1. 36
      server/app/com/xsn/explorer/services/BlockService.scala
  2. 72
      server/app/com/xsn/explorer/services/logic/BlockLogic.scala
  3. 10
      server/test/com/xsn/explorer/helpers/BlockLoader.scala
  4. 10
      server/test/com/xsn/explorer/helpers/TransactionLoader.scala
  5. 52
      server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala
  6. 50
      server/test/controllers/BlocksControllerSpec.scala
  7. 21
      server/test/resources/blocks/a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c
  8. 69
      server/test/resources/transactions/99c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8
  9. 94
      server/test/resources/transactions/9f2cef5c3f94197247d0109e224e428d7904e54f4c695433829bbde6ef99ea44

36
server/app/com/xsn/explorer/services/BlockService.scala

@ -6,10 +6,11 @@ import com.alexitc.playsonify.core.FutureApplicationResult
import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps}
import com.xsn.explorer.errors.BlockNotFoundError
import com.xsn.explorer.models._
import com.xsn.explorer.models.rpc.Block
import com.xsn.explorer.models.rpc.{Block, TransactionVIN}
import com.xsn.explorer.services.logic.{BlockLogic, TransactionLogic}
import org.scalactic.Good
import scala.concurrent.ExecutionContext
import scala.concurrent.{ExecutionContext, Future}
class BlockService @Inject() (
xsnService: XSNService,
@ -106,7 +107,6 @@ class BlockService @Inject() (
result.toFuture
}
// TODO: Handle blocks with coin split
private def getTPoSBlockRewards(block: Block): FutureApplicationResult[BlockRewards] = {
val result = for {
coinstakeTxId <- blockLogic
@ -119,12 +119,7 @@ class BlockService @Inject() (
.getVIN(coinstakeTx, BlockNotFoundError)
.toFutureOr
previousToCoinstakeTx <- xsnService
.getTransaction(coinstakeTxVIN.txid)
.toFutureOr
previousToCoinstakeVOUT <- transactionLogic
.getVOUT(coinstakeTxVIN, previousToCoinstakeTx, BlockNotFoundError)
.toFutureOr
coinstakeInput <- getCoinstakeInput(coinstakeTxVIN).toFutureOr
tposTxId <- blockLogic
.getTPoSTransactionId(block)
@ -140,10 +135,31 @@ class BlockService @Inject() (
(ownerAddress, merchantAddress) = addresses
rewards <- blockLogic
.getTPoSRewards(coinstakeTx, ownerAddress, merchantAddress, previousToCoinstakeVOUT.value)
.getTPoSRewards(coinstakeTx, ownerAddress, merchantAddress, coinstakeInput)
.toFutureOr
} yield rewards
result.toFuture
}
private def getCoinstakeInput(coinstakeTxVIN: TransactionVIN): FutureApplicationResult[BigDecimal] = {
def loadFromTx = {
val result = for {
previousToCoinstakeTx <- xsnService
.getTransaction(coinstakeTxVIN.txid)
.toFutureOr
previousToCoinstakeVOUT <- transactionLogic
.getVOUT(coinstakeTxVIN, previousToCoinstakeTx, BlockNotFoundError)
.toFutureOr
} yield previousToCoinstakeVOUT.value
result.toFuture
}
coinstakeTxVIN
.value
.map(Good(_))
.map(Future.successful)
.getOrElse(loadFromTx)
}
}

72
server/app/com/xsn/explorer/services/logic/BlockLogic.scala

@ -98,47 +98,49 @@ class BlockLogic {
}
}
// 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
/**
* While we expected the following
* - 1st output is empty and it is removed.
* - 3 outputs, normal TPoS
* - 4 outputs, coin split TPoS
*
* In order to display partial solutions, we will just filter by the
* addresses to get the rewards.
*/
val coinstakeVOUT = coinstakeTx.vout
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))
}
}

10
server/test/com/xsn/explorer/helpers/BlockLoader.scala

@ -12,8 +12,12 @@ object BlockLoader {
}
def json(blockhash: String): JsValue = {
val resource = s"$BasePath/$blockhash"
val json = scala.io.Source.fromResource(resource).getLines().mkString("\n")
Json.parse(json)
try {
val resource = s"$BasePath/$blockhash"
val json = scala.io.Source.fromResource(resource).getLines().mkString("\n")
Json.parse(json)
} catch {
case _ => throw new RuntimeException(s"Block $blockhash not found")
}
}
}

10
server/test/com/xsn/explorer/helpers/TransactionLoader.scala

@ -12,8 +12,12 @@ object TransactionLoader {
}
def json(txid: String): JsValue = {
val resource = s"$BasePath/$txid"
val json = scala.io.Source.fromResource(resource).getLines().mkString("\n")
Json.parse(json)
try {
val resource = s"$BasePath/$txid"
val json = scala.io.Source.fromResource(resource).getLines().mkString("\n")
Json.parse(json)
} catch {
case _ => throw new RuntimeException(s"Transaction $txid not found")
}
}
}

52
server/test/com/xsn/explorer/services/XSNServiceRPCImplSpec.scala

@ -2,7 +2,7 @@ package com.xsn.explorer.services
import com.xsn.explorer.config.RPCConfig
import com.xsn.explorer.errors._
import com.xsn.explorer.helpers.{DataHelper, Executors, TransactionLoader}
import com.xsn.explorer.helpers.{BlockLoader, DataHelper, Executors, TransactionLoader}
import com.xsn.explorer.models.{Address, Blockhash}
import org.mockito.ArgumentMatchers._
import org.mockito.Mockito._
@ -257,34 +257,8 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures
"getBlock" should {
"return a block" in {
val responseBody =
"""
|{
| "result": {
| "hash": "b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81",
| "confirmations": 11163,
| "size": 478,
| "height": 809,
| "version": 536870912,
| "merkleroot": "598cc6ba8238d87641b0dbd02485b7d635b5417429df3145c98c3ff8779ab4b8",
| "tx": [
| "7f12adbb63d443502cf151c76946d5faa0b1c662a5d67afc7da085c74e06f1ce",
| "0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641"
| ],
| "time": 1520318120,
| "mediantime": 1520318054,
| "nonce": 0,
| "bits": "1d011212",
| "difficulty": 0.9340526210769362,
| "chainwork": "00000000000000000000000000000000000000000000000000000084c71ff420",
| "previousblockhash": "9490ce5d14bb5e79a790ddede03fc3a9bde3f7156f34a57ae3ceb56ae7426c14",
| "nextblockhash": "f6e3199c241131e79640fe027a6ef993c02b3520c3d4ba08cd67abfbb98ec07e"
| },
| "error": null,
| "id": null
|}
|""".stripMargin
val block = BlockLoader.json("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81")
val responseBody = createRPCSuccessfulResponse(block)
val blockhash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81").get
val json = Json.parse(responseBody)
@ -302,6 +276,26 @@ class XSNServiceRPCImplSpec extends WordSpec with MustMatchers with ScalaFutures
}
}
"return a TPoS block" in {
val block = BlockLoader.json("a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c")
val responseBody = createRPCSuccessfulResponse(block)
val blockhash = Blockhash.from("a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c").get
val json = Json.parse(responseBody)
when(response.status).thenReturn(200)
when(response.json).thenReturn(json)
when(request.post[String](anyString())(any())).thenReturn(Future.successful(response))
whenReady(service.getBlock(blockhash)) { result =>
result.isGood mustEqual true
val block = result.get
block.hash.string mustEqual blockhash.string
block.transactions.size mustEqual 2
}
}
"fail on unknown block" in {
val responseBody = """{"result":null,"error":{"code":-5,"message":"Block not found"},"id":null}"""

50
server/test/controllers/BlocksControllerSpec.scala

@ -33,6 +33,10 @@ class BlocksControllerSpec extends MyAPISpec {
val tposBlockCoinstakeTx = TransactionLoader.get("8c7feafc18576b89bf87faf8aa89feaac1a3fad7d5da77d1fe773219a0e9d864")
val tposBlockCoinstakeTxInput = TransactionLoader.get("9ecf10916467dccc8c8f3a87d869dc5aceb57d5d1c2117036fe60f31369a284e")
val tposBlock2 = BlockLoader.get("a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c")
val tposBlock2ContractTx = TransactionLoader.get(tposBlock2.tposContract.get.string)
val tposBlock2CoinstakeTx = TransactionLoader.get(tposBlock2.transactions(1).string)
// PoW
val powBlock = BlockLoader.get("000004645e2717b556682e3c642a4c6e473bf25c653ff8e8c114a3006040ffb8")
val powBlockPreviousTx = TransactionLoader.get("67aa0bd8b9297ca6ee25a1e5c2e3a8dbbcc1e20eab76b6d1bdf9d69f8a5356b8")
@ -42,6 +46,7 @@ class BlocksControllerSpec extends MyAPISpec {
posBlock.hash -> posBlock,
posBlockRoundingError.hash -> posBlockRoundingError,
tposBlock.hash -> tposBlock,
tposBlock2.hash -> tposBlock2,
powBlock.hash -> powBlock
)
@ -63,7 +68,9 @@ class BlocksControllerSpec extends MyAPISpec {
powBlockPreviousTx.id -> powBlockPreviousTx,
tposBlockContractTx.id -> tposBlockContractTx,
tposBlockCoinstakeTx.id -> tposBlockCoinstakeTx,
tposBlockCoinstakeTxInput.id -> tposBlockCoinstakeTxInput
tposBlockCoinstakeTxInput.id -> tposBlockCoinstakeTxInput,
tposBlock2CoinstakeTx.id -> tposBlock2CoinstakeTx,
tposBlock2ContractTx.id -> tposBlock2ContractTx
)
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = {
@ -183,7 +190,7 @@ class BlocksControllerSpec extends MyAPISpec {
"retrieve TPoS block" in {
val block = tposBlock
val response = GET(url("19f320185015d146237efe757852b21c5e08b88b2f4de9d3fa9517d8463e472b"))
val response = GET(url(block.hash.string))
status(response) mustEqual OK
@ -221,6 +228,45 @@ class BlocksControllerSpec extends MyAPISpec {
}
"retrieve TPoS block with coinsplit" in {
val block = tposBlock2
val response = GET(url(block.hash.string))
status(response) mustEqual OK
val json = contentAsJson(response)
val jsonBlock = (json \ "block").as[JsValue]
val jsonRewards = (json \ "rewards").as[JsValue]
(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 "Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge"
(jsonOwner \ "value").as[BigDecimal] mustEqual BigDecimal("22.275")
val jsonMerchant = (jsonRewards \ "merchant").as[JsValue]
(jsonMerchant \ "address").as[String] mustEqual "XbGFpsuhv6AH3gp3dx5eQrAexP5kESh9bY"
(jsonMerchant \ "value").as[BigDecimal] mustEqual BigDecimal("0.225")
val jsonMasternode = (jsonRewards \ "masternode").as[JsValue]
(jsonMasternode \ "address").as[String] mustEqual "Xc3bKuGzy9grJZxC2ieTgQjjgyTMKSLqSM"
(jsonMasternode \ "value").as[BigDecimal] mustEqual BigDecimal("22.5")
}
"fail on the wrong blockhash format" in {
val response = GET(url("000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5"))

21
server/test/resources/blocks/a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c

@ -0,0 +1,21 @@
{
"hash": "a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c",
"confirmations": 23,
"size": 441,
"height": 37588,
"version": 536870912,
"merkleroot": "13f3f7171cf38573e516afb77c78107859352cc55e6bfd3f6f9a127ef93221e5",
"tx": [
"c5e1884d89050016a8471ee0239f63b22fc0153111427b17fc9e49b1684663e3",
"9f2cef5c3f94197247d0109e224e428d7904e54f4c695433829bbde6ef99ea44"
],
"time": 1522677340,
"mediantime": 1522676874,
"nonce": 0,
"bits": "1c034b3c",
"difficulty": 77.71860581466451,
"chainwork": "000000000000000000000000000000000000000000000000000f8121abc522cc",
"tposcontract": "99c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8",
"previousblockhash": "be885c982c30a1362f92085c35b8249f516f3e907f17b4708d2f418bed8030a4",
"nextblockhash": "a5b60495db041167486f85b652da7312d41d68198b81ca282d4b7f0bca87bd97"
}

69
server/test/resources/transactions/99c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8

@ -0,0 +1,69 @@
{
"hex": "0100000001e4351d959235468eb25b4c32b129e2d2a2fdf09fd72e7172f46822e6e21e6279000000006a4730440220717ffda2fc31e25849b7b6216d777c50135ae2c24f81ea74c63230c00b34772702203a35a8a3a15493b5de5d68d3586fb090f9bed63bb20078b2ee868a61d3c9cb9b012102cf95793cb0145551c9a6572e3eb2c3bcf524298b4f217065b771e06925e30291feffffff030000000000000000496a22587535556b67524c385952716f57367545573853784d4c446b4a77626a4656666765225862474670737568763641483367703364783565517241657850356b455368396259016300e1f505000000001976a914c9ba3c497107845ea80dcf64951ced030556384e88accd01164e020000001976a914690b8f4edf066edb2fa3385f0d3e39771d27b6d088ac12370000",
"txid": "99c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8",
"size": 307,
"version": 1,
"locktime": 14098,
"vin": [
{
"txid": "79621ee2e62268f472712ed79ff0fda2d2e229b1324c5bb28e463592951d35e4",
"vout": 0,
"scriptSig": {
"asm": "30440220717ffda2fc31e25849b7b6216d777c50135ae2c24f81ea74c63230c00b34772702203a35a8a3a15493b5de5d68d3586fb090f9bed63bb20078b2ee868a61d3c9cb9b[ALL] 02cf95793cb0145551c9a6572e3eb2c3bcf524298b4f217065b771e06925e30291",
"hex": "4730440220717ffda2fc31e25849b7b6216d777c50135ae2c24f81ea74c63230c00b34772702203a35a8a3a15493b5de5d68d3586fb090f9bed63bb20078b2ee868a61d3c9cb9b012102cf95793cb0145551c9a6572e3eb2c3bcf524298b4f217065b771e06925e30291"
},
"value": 100.00000000,
"valueSat": 10000000000,
"address": "Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge",
"sequence": 4294967294
}
],
"vout": [
{
"value": 0.00000000,
"valueSat": 0,
"n": 0,
"scriptPubKey": {
"asm": "OP_RETURN 587535556b67524c385952716f57367545573853784d4c446b4a77626a4656666765 5862474670737568763641483367703364783565517241657850356b455368396259 99",
"hex": "6a22587535556b67524c385952716f57367545573853784d4c446b4a77626a4656666765225862474670737568763641483367703364783565517241657850356b4553683962590163",
"type": "nulldata"
}
},
{
"value": 1.00000000,
"valueSat": 100000000,
"n": 1,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 c9ba3c497107845ea80dcf64951ced030556384e OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914c9ba3c497107845ea80dcf64951ced030556384e88ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge"
]
}
},
{
"value": 98.99999693,
"valueSat": 9899999693,
"n": 2,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 690b8f4edf066edb2fa3385f0d3e39771d27b6d0 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914690b8f4edf066edb2fa3385f0d3e39771d27b6d088ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"XkGGdarLNe8CtrCLg15QkcrgSm4td8vPoz"
]
},
"spentTxId": "1183105915752a7559453775a1e663317bebb9fb35f0880129a78933f2fb2fa1",
"spentIndex": 0,
"spentHeight": 18357
}
],
"blockhash": "ad92f0dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981",
"height": 14099,
"confirmations": 23566,
"time": 1521230973,
"blocktime": 1521230973
}

94
server/test/resources/transactions/9f2cef5c3f94197247d0109e224e428d7904e54f4c695433829bbde6ef99ea44

@ -0,0 +1,94 @@
{
"hex": "0100000001ad64f9096a74457e97ce20600bb0b80914326f6fdfa1df5a9ada54fdf4cd96f20200000000ffffffff05000000000000000000bcbfd4c81d0000001976a914c9ba3c497107845ea80dcf64951ced030556384e88acdccb0f441d0000001976a914c9ba3c497107845ea80dcf64951ced030556384e88aca0525701000000001976a9140651c16ab25fa5a6b8c0598b6fd18c493ad2570088ac80461c86000000001976a9140ee4c07ec4c237dbc8569d483eb8ab0c3246314188ac00000000",
"txid": "9f2cef5c3f94197247d0109e224e428d7904e54f4c695433829bbde6ef99ea44",
"size": 196,
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "f296cdf4fd54da9a5adfa1df6f6f321409b8b00b6020ce977e45746a09f964ad",
"vout": 2,
"scriptSig": {
"asm": "",
"hex": ""
},
"value": 2513.91875000,
"valueSat": 251391875000,
"address": "Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge",
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.00000000,
"valueSat": 0,
"n": 0,
"scriptPubKey": {
"asm": "",
"hex": "",
"type": "nonstandard"
}
},
{
"value": 1279.23437500,
"valueSat": 127923437500,
"n": 1,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 c9ba3c497107845ea80dcf64951ced030556384e OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914c9ba3c497107845ea80dcf64951ced030556384e88ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge"
]
}
},
{
"value": 1256.95937500,
"valueSat": 125695937500,
"n": 2,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 c9ba3c497107845ea80dcf64951ced030556384e OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914c9ba3c497107845ea80dcf64951ced030556384e88ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"Xu5UkgRL8YRqoW6uEW8SxMLDkJwbjFVfge"
]
}
},
{
"value": 0.22500000,
"valueSat": 22500000,
"n": 3,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 0651c16ab25fa5a6b8c0598b6fd18c493ad25700 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a9140651c16ab25fa5a6b8c0598b6fd18c493ad2570088ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"XbGFpsuhv6AH3gp3dx5eQrAexP5kESh9bY"
]
}
},
{
"value": 22.50000000,
"valueSat": 2250000000,
"n": 4,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 0ee4c07ec4c237dbc8569d483eb8ab0c32463141 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a9140ee4c07ec4c237dbc8569d483eb8ab0c3246314188ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"Xc3bKuGzy9grJZxC2ieTgQjjgyTMKSLqSM"
]
}
}
],
"blockhash": "a3a9fb111a3f85c3d920c2dc58ce14d541a65763834247ef958aa3b4d665ef9c",
"height": 37588,
"confirmations": 48,
"time": 1522677340,
"blocktime": 1522677340
}
Loading…
Cancel
Save