Alexis Hernandez
7 years ago
8 changed files with 432 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||
|
package com.xsn.explorer.models |
||||
|
|
||||
|
import play.api.libs.json.{Json, Writes} |
||||
|
|
||||
|
case class BlockDetails(block: Block, rewards: BlockRewards) |
||||
|
|
||||
|
object BlockDetails { |
||||
|
|
||||
|
implicit val reads: Writes[BlockDetails] = Json.writes[BlockDetails] |
||||
|
|
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
package com.xsn.explorer.models |
||||
|
|
||||
|
import play.api.libs.json.{Json, Writes} |
||||
|
|
||||
|
case class BlockReward(address: Address, value: BigDecimal) |
||||
|
|
||||
|
object BlockReward { |
||||
|
implicit val writes: Writes[BlockReward] = Json.writes[BlockReward] |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
package com.xsn.explorer.models |
||||
|
|
||||
|
import play.api.libs.json.{Json, Writes} |
||||
|
|
||||
|
case class BlockRewards(coinstake: BlockReward, masternode: Option[BlockReward]) |
||||
|
|
||||
|
object BlockRewards { |
||||
|
implicit val writes: Writes[BlockRewards] = Json.writes[BlockRewards] |
||||
|
} |
@ -0,0 +1,56 @@ |
|||||
|
package com.xsn.explorer.services |
||||
|
|
||||
|
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 |
||||
|
import com.xsn.explorer.models._ |
||||
|
import com.xsn.explorer.services.logic.{BlockLogic, TransactionLogic} |
||||
|
|
||||
|
import scala.concurrent.ExecutionContext |
||||
|
|
||||
|
class BlockService @Inject() ( |
||||
|
xsnService: XSNService, |
||||
|
blockLogic: BlockLogic, |
||||
|
transactionLogic: TransactionLogic)( |
||||
|
implicit ec: ExecutionContext) { |
||||
|
|
||||
|
def getDetails(blockhashString: String): FutureApplicationResult[BlockDetails] = { |
||||
|
val result = for { |
||||
|
blockhash <- blockLogic |
||||
|
.getBlockhash(blockhashString) |
||||
|
.toFutureOr |
||||
|
block <- xsnService |
||||
|
.getBlock(blockhash) |
||||
|
.toFutureOr |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
coinstakeAddress <- transactionLogic |
||||
|
.getAddress(previousToCoinstakeVOUT, BlockNotFoundError) |
||||
|
.toFutureOr |
||||
|
|
||||
|
rewards <- blockLogic |
||||
|
.getRewards(coinstakeTx, coinstakeAddress, previousToCoinstakeVOUT.value) |
||||
|
.toFutureOr |
||||
|
} yield BlockDetails(block, rewards) |
||||
|
|
||||
|
result.toFuture |
||||
|
} |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
package com.xsn.explorer.services.logic |
||||
|
|
||||
|
import com.alexitc.playsonify.core.ApplicationResult |
||||
|
import com.xsn.explorer.errors.{BlockNotFoundError, BlockhashFormatError} |
||||
|
import com.xsn.explorer.models._ |
||||
|
import org.scalactic.{Bad, Good, One, Or} |
||||
|
|
||||
|
class BlockLogic { |
||||
|
|
||||
|
def getBlockhash(string: String): ApplicationResult[Blockhash] = { |
||||
|
val maybe = Blockhash.from(string) |
||||
|
Or.from(maybe, One(BlockhashFormatError)) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the coinstake transaction id for the given block. |
||||
|
* |
||||
|
* A PoS block contains at least 2 transactions: |
||||
|
* - the 1st one is empty |
||||
|
* - the 2nd one is the Coinstake transaction. |
||||
|
*/ |
||||
|
def getCoinstakeTransactionId(block: Block): ApplicationResult[TransactionId] = { |
||||
|
val maybe = block.transactions.lift(1) |
||||
|
|
||||
|
Or.from(maybe, One(BlockNotFoundError)) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computes the rewards for a PoS coinstake transaction. |
||||
|
* |
||||
|
* There should be a coinstake reward and possibly a master node reward. |
||||
|
* |
||||
|
* The rewards are computed based on the transaction output which is expected to |
||||
|
* contain between 2 and 4 values: |
||||
|
* - the 1st one is empty |
||||
|
* - the 2nd one goes to the coinstake |
||||
|
* - the 3rd one (if present) will go to the coinstake if the address matches, otherwise it goes to master node. |
||||
|
* - the 4th one (if present) will go to the master node. |
||||
|
* |
||||
|
* While the previous format should be meet by the RPC server, we compute the rewards |
||||
|
* based on coinstake address. |
||||
|
* |
||||
|
* Sometimes there could be rounding errors, for example, when the input is not exactly divisible by 2, |
||||
|
* we return 0 in that case because the reward could be negative. |
||||
|
*/ |
||||
|
def getRewards( |
||||
|
coinstakeTx: Transaction, |
||||
|
coinstakeAddress: Address, |
||||
|
coinstakeInput: BigDecimal): ApplicationResult[BlockRewards] = { |
||||
|
|
||||
|
// first vout is empty, useless |
||||
|
val coinstakeVOUT = coinstakeTx.vout.drop(1) |
||||
|
if (coinstakeVOUT.size >= 1 && coinstakeVOUT.size <= 3) { |
||||
|
val value = coinstakeVOUT |
||||
|
.filter(_.address contains coinstakeAddress) |
||||
|
.map(_.value) |
||||
|
.sum |
||||
|
|
||||
|
val coinstakeReward = BlockReward( |
||||
|
coinstakeAddress, |
||||
|
(value - coinstakeInput) max 0) |
||||
|
|
||||
|
val masternodeRewardOUT = coinstakeVOUT.filterNot(_.address contains coinstakeAddress) |
||||
|
val masternodeAddressMaybe = masternodeRewardOUT.flatMap(_.address).headOption |
||||
|
val masternodeRewardMaybe = masternodeAddressMaybe.map { masternodeAddress => |
||||
|
BlockReward( |
||||
|
masternodeAddress, |
||||
|
masternodeRewardOUT.filter(_.address contains masternodeAddress).map(_.value).sum |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Good(BlockRewards(coinstakeReward, masternodeRewardMaybe)) |
||||
|
} else { |
||||
|
Bad(BlockNotFoundError).accumulating |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
package controllers |
||||
|
|
||||
|
import javax.inject.Inject |
||||
|
|
||||
|
import com.xsn.explorer.services.BlockService |
||||
|
import controllers.common.{MyJsonController, MyJsonControllerComponents} |
||||
|
|
||||
|
class BlocksController @Inject() ( |
||||
|
blockService: BlockService, |
||||
|
cc: MyJsonControllerComponents) |
||||
|
extends MyJsonController(cc) { |
||||
|
|
||||
|
def getDetails(blockhash: String) = publicNoInput { _ => |
||||
|
blockService.getDetails(blockhash) |
||||
|
} |
||||
|
} |
@ -0,0 +1,252 @@ |
|||||
|
package controllers |
||||
|
|
||||
|
import com.alexitc.playsonify.PublicErrorRenderer |
||||
|
import com.alexitc.playsonify.core.FutureApplicationResult |
||||
|
import com.xsn.explorer.errors.{BlockNotFoundError, TransactionNotFoundError} |
||||
|
import com.xsn.explorer.helpers.DummyXSNService |
||||
|
import com.xsn.explorer.models._ |
||||
|
import com.xsn.explorer.services.XSNService |
||||
|
import controllers.common.MyAPISpec |
||||
|
import org.scalactic.{Bad, Good} |
||||
|
import play.api.inject.bind |
||||
|
import play.api.libs.json.JsValue |
||||
|
import play.api.test.Helpers._ |
||||
|
|
||||
|
import scala.concurrent.Future |
||||
|
|
||||
|
class BlocksControllerSpec extends MyAPISpec { |
||||
|
|
||||
|
// PoS block |
||||
|
val posBlock = createBlock( |
||||
|
hash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81").get, |
||||
|
transactions = List( |
||||
|
TransactionId.from("7f12adbb63d443502cf151c76946d5faa0b1c662a5d67afc7da085c74e06f1ce").get, |
||||
|
TransactionId.from("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641").get |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val posBlockCoinstakeTx = createTx( |
||||
|
id = TransactionId.from("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641").get, |
||||
|
vin = TransactionVIN(TransactionId.from("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9").get, 2), |
||||
|
vout = List( |
||||
|
TransactionVOUT(BigDecimal("0"), 0, "nonstandard", None), |
||||
|
TransactionVOUT(n = 1, value = BigDecimal("600"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")), |
||||
|
TransactionVOUT(n = 2, value = BigDecimal("600"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")), |
||||
|
TransactionVOUT(BigDecimal("10"), 3, "pubkeyhash", Some(Address.from("XnH3bC9NruJ4wnu4Dgi8F3wemmJtcxpKp6").get)) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val posBlockCoinstakeTxInput = createTx( |
||||
|
id = TransactionId.from("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9").get, |
||||
|
vin = TransactionVIN(TransactionId.from("fd74206866fc4ed986d39084eb9f20de6cb324b028693f33d60897ac995fff4f").get, 2), |
||||
|
vout = List( |
||||
|
TransactionVOUT(BigDecimal("0"), 0, "nonstandard", None), |
||||
|
TransactionVOUT(BigDecimal("1"), 1, "pubkeyhash", Some(Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL").get)), |
||||
|
TransactionVOUT(BigDecimal("1000"), 2, "pubkeyhash", Some(Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL").get)) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
// PoS block with rounding error |
||||
|
val posBlockRoundingError = createBlock( |
||||
|
hash = Blockhash.from("25762bf01143f7fe34912c926e0b95528b082c6323de35516de0fc321f5d8058").get, |
||||
|
transactions = List( |
||||
|
TransactionId.from("df275d713fcf5e78e7e8369d640201d46736c0d2255e31ce45bd5aa0206f861f").get, |
||||
|
TransactionId.from("0b761343c7be39116d5429953e0cfbf51bfe83400ab27d61084222451045116c").get |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val posBlockRoundingErrorCoinstakeTx = createTx( |
||||
|
id = TransactionId.from("0b761343c7be39116d5429953e0cfbf51bfe83400ab27d61084222451045116c").get, |
||||
|
vin = TransactionVIN(TransactionId.from("1860288a5a87c79e617f743af44600e050c28ddb7d929d93d43a9148e2ba6638").get, 1), |
||||
|
vout = List( |
||||
|
TransactionVOUT(BigDecimal("0"), 0, "nonstandard", None), |
||||
|
TransactionVOUT(n = 1, value = BigDecimal("292968.74570312"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")), |
||||
|
TransactionVOUT(n = 2, value = BigDecimal("292968.74570312"), scriptPubKeyType = "pubkeyhash", address = Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL")) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val posBlockRoundingErrorCoinstakeTxInput = createTx( |
||||
|
id = TransactionId.from("1860288a5a87c79e617f743af44600e050c28ddb7d929d93d43a9148e2ba6638").get, |
||||
|
vin = TransactionVIN(TransactionId.from("ef157f5ec0b3a6cdf669ff799988ee94d9fa2af8adaf2408ae9e34b47310831f").get, 2), |
||||
|
vout = List( |
||||
|
TransactionVOUT(BigDecimal("0"), 0, "nonstandard", None), |
||||
|
TransactionVOUT(BigDecimal("585937.49140625"), 1, "pubkeyhash", Some(Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL").get)), |
||||
|
TransactionVOUT(BigDecimal("585937.49140625"), 2, "pubkeyhash", Some(Address.from("XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL").get)) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val customXSNService = new DummyXSNService { |
||||
|
val blocks = Map( |
||||
|
posBlock.hash -> posBlock, |
||||
|
posBlockRoundingError.hash -> posBlockRoundingError |
||||
|
) |
||||
|
|
||||
|
override def getBlock(blockhash: Blockhash): FutureApplicationResult[Block] = { |
||||
|
val result = blocks.get(blockhash) |
||||
|
.map(Good(_)) |
||||
|
.getOrElse { |
||||
|
Bad(BlockNotFoundError).accumulating |
||||
|
} |
||||
|
|
||||
|
Future.successful(result) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
val txs = Map( |
||||
|
posBlockCoinstakeTx.id -> posBlockCoinstakeTx, |
||||
|
posBlockCoinstakeTxInput.id -> posBlockCoinstakeTxInput, |
||||
|
posBlockRoundingErrorCoinstakeTx.id -> posBlockRoundingErrorCoinstakeTx, |
||||
|
posBlockRoundingErrorCoinstakeTxInput.id -> posBlockRoundingErrorCoinstakeTxInput |
||||
|
) |
||||
|
|
||||
|
override def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { |
||||
|
val result = txs.get(txid) |
||||
|
.map(Good(_)) |
||||
|
.getOrElse { |
||||
|
Bad(TransactionNotFoundError).accumulating |
||||
|
} |
||||
|
|
||||
|
Future.successful(result) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override val application = guiceApplicationBuilder |
||||
|
.overrides(bind[XSNService].to(customXSNService)) |
||||
|
.build() |
||||
|
|
||||
|
"GET /blocks/:blockhash" should { |
||||
|
def url(blockhash: String) = s"/blocks/$blockhash" |
||||
|
|
||||
|
"retrieve a PoS block" in { |
||||
|
val block = posBlock |
||||
|
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").as[Blockhash] mustEqual block.previousBlockhash |
||||
|
(jsonBlock \ "nextBlockhash").as[Blockhash] mustEqual block.nextBlockhash |
||||
|
|
||||
|
val jsonCoinstake = (jsonRewards \ "coinstake").as[JsValue] |
||||
|
(jsonCoinstake \ "address").as[String] mustEqual "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" |
||||
|
(jsonCoinstake \ "value").as[BigDecimal] mustEqual BigDecimal("200") |
||||
|
|
||||
|
val jsonMasternode = (jsonRewards \ "masternode").as[JsValue] |
||||
|
(jsonMasternode \ "address").as[String] mustEqual "XnH3bC9NruJ4wnu4Dgi8F3wemmJtcxpKp6" |
||||
|
(jsonMasternode \ "value").as[BigDecimal] mustEqual BigDecimal("10") |
||||
|
} |
||||
|
|
||||
|
"retrieve a PoS block having a rounding error" in { |
||||
|
val block = posBlockRoundingError |
||||
|
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").as[Blockhash] mustEqual block.previousBlockhash |
||||
|
(jsonBlock \ "nextBlockhash").as[Blockhash] mustEqual block.nextBlockhash |
||||
|
|
||||
|
val jsonCoinstake = (jsonRewards \ "coinstake").as[JsValue] |
||||
|
(jsonCoinstake \ "address").as[String] mustEqual "XgEGH3y7RfeKEdn2hkYEvBnrnmGBr7zvjL" |
||||
|
(jsonCoinstake \ "value").as[BigDecimal] mustEqual BigDecimal("0") |
||||
|
|
||||
|
val jsonMasternode = (jsonRewards \ "masternode").asOpt[JsValue] |
||||
|
jsonMasternode.isEmpty mustEqual true |
||||
|
} |
||||
|
|
||||
|
"fail on the wrong blockhash format" in { |
||||
|
val response = GET(url("000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5")) |
||||
|
|
||||
|
status(response) mustEqual BAD_REQUEST |
||||
|
|
||||
|
val json = contentAsJson(response) |
||||
|
val errorList = (json \ "errors").as[List[JsValue]] |
||||
|
|
||||
|
errorList.size mustEqual 1 |
||||
|
val error = errorList.head |
||||
|
|
||||
|
(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")) |
||||
|
|
||||
|
status(response) mustEqual BAD_REQUEST |
||||
|
|
||||
|
val json = contentAsJson(response) |
||||
|
val errorList = (json \ "errors").as[List[JsValue]] |
||||
|
|
||||
|
errorList.size mustEqual 1 |
||||
|
val error = errorList.head |
||||
|
|
||||
|
(error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType |
||||
|
(error \ "field").as[String] mustEqual "blockhash" |
||||
|
(error \ "message").as[String].nonEmpty mustEqual true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private def createTx(id: TransactionId, vin: TransactionVIN, vout: List[TransactionVOUT]): Transaction = { |
||||
|
Transaction( |
||||
|
id = id, |
||||
|
size = Size(234), |
||||
|
blockhash = Blockhash.from("b72dd1655408e9307ef5874be20422ee71029333283e2360975bc6073bdb2b81").get, |
||||
|
time = 1520318120, |
||||
|
blocktime = 1520318120, |
||||
|
confirmations = Confirmations(1950), |
||||
|
vin = Some(vin), |
||||
|
vout = vout |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
private def createBlock(hash: Blockhash, transactions: List[TransactionId]): Block = { |
||||
|
Block( |
||||
|
hash = hash, |
||||
|
transactions = transactions, |
||||
|
confirmations = Confirmations(11189), |
||||
|
size = Size(478), |
||||
|
height = Height(809), |
||||
|
version = 536870912, |
||||
|
merkleRoot = Blockhash.from("598cc6ba8238d87641b0dbd02485b7d635b5417429df3145c98c3ff8779ab4b8").get, |
||||
|
time = 1520318054, |
||||
|
medianTime = 1520318054, |
||||
|
nonce = 0, |
||||
|
bits = "1d011212", |
||||
|
chainwork = "00000000000000000000000000000000000000000000000000000084c71ff420", |
||||
|
difficulty = BigDecimal("0.9340526210769362"), |
||||
|
previousBlockhash = Blockhash.from("000003dc4c2fc449dededaaad6efc33ce1b64b88a060652dc47edc63d6d6b524").get, |
||||
|
nextBlockhash = Blockhash.from("000000125c06cedf38b07bff174bdb61027935dbcb34831d28cff40bedb519d5").get |
||||
|
) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue