mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
Add basic esplora client (#4018)
* Add basic esplora client * Improve error message
This commit is contained in:
parent
cf16d93648
commit
03abb7537b
2
.github/workflows/Linux_2.12_RPC_Tests.yml
vendored
2
.github/workflows/Linux_2.12_RPC_Tests.yml
vendored
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.12.15 downloadEclair downloadLnd downloadCLightning coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test clightningRpcTest/test
|
||||
run: sbt ++2.12.15 coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test clightningRpcTest/test esploraTest/test
|
||||
|
2
.github/workflows/Linux_2.13_RPC_Tests.yml
vendored
2
.github/workflows/Linux_2.13_RPC_Tests.yml
vendored
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.8 coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test clightningRpcTest/test
|
||||
run: sbt ++2.13.8 coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test clightningRpcTest/test esploraTest/test
|
||||
|
2
.github/workflows/Mac_2.13_RPC_Tests.yml
vendored
2
.github/workflows/Mac_2.13_RPC_Tests.yml
vendored
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.8 coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test
|
||||
run: sbt ++2.13.8 coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test esploraTest/test
|
||||
|
2
.github/workflows/TorTests.yml
vendored
2
.github/workflows/TorTests.yml
vendored
@ -30,4 +30,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.8 torTest/test nodeTest/test dlcNodeTest/test
|
||||
run: sbt ++2.13.8 torTest/test nodeTest/test dlcNodeTest/test esploraTest/test
|
||||
|
25
build.sbt
25
build.sbt
@ -30,7 +30,8 @@ lazy val commonJsSettings = {
|
||||
scalaJSLinkerConfig ~= {
|
||||
_.withModuleKind(ModuleKind.CommonJSModule)
|
||||
}
|
||||
) ++ CommonSettings.settings ++ Seq(scalacOptions += "-P:scalajs:nowarnGlobalExecutionContext")
|
||||
) ++ CommonSettings.settings ++ Seq(
|
||||
scalacOptions += "-P:scalajs:nowarnGlobalExecutionContext")
|
||||
}
|
||||
|
||||
lazy val crypto = crossProject(JVMPlatform, JSPlatform)
|
||||
@ -184,6 +185,8 @@ lazy val `bitcoin-s` = project
|
||||
dbCommonsTest,
|
||||
feeProvider,
|
||||
feeProviderTest,
|
||||
esplora,
|
||||
esploraTest,
|
||||
dlcOracle,
|
||||
dlcOracleTest,
|
||||
dlcTest,
|
||||
@ -242,6 +245,8 @@ lazy val `bitcoin-s` = project
|
||||
dbCommonsTest,
|
||||
feeProvider,
|
||||
feeProviderTest,
|
||||
esplora,
|
||||
esploraTest,
|
||||
dlcOracle,
|
||||
dlcOracleTest,
|
||||
dlcTest,
|
||||
@ -483,6 +488,24 @@ lazy val dbCommonsTest = project
|
||||
)
|
||||
.dependsOn(testkit)
|
||||
|
||||
lazy val esplora = project
|
||||
.in(file("esplora"))
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
.settings(
|
||||
name := "bitcoin-s-esplora",
|
||||
libraryDependencies ++= Deps.esplora.value
|
||||
)
|
||||
.dependsOn(coreJVM, appCommons, tor)
|
||||
|
||||
lazy val esploraTest = project
|
||||
.in(file("esplora-test"))
|
||||
.settings(CommonSettings.testSettings: _*)
|
||||
.settings(
|
||||
name := "bitcoin-s-esplora-test",
|
||||
libraryDependencies ++= Deps.esploraTest.value
|
||||
)
|
||||
.dependsOn(coreJVM % testAndCompile, esplora, testkit)
|
||||
|
||||
lazy val feeProvider = project
|
||||
.in(file("fee-provider"))
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
|
@ -0,0 +1,164 @@
|
||||
package org.bitcoins.esplora
|
||||
|
||||
import org.bitcoins.core.config.MainNet
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.crypto._
|
||||
import org.bitcoins.testkit.util.BitcoinSAsyncTest
|
||||
import org.bitcoins.testkit.util.TorUtil._
|
||||
import org.bitcoins.tor.Socks5ProxyParams
|
||||
|
||||
class EsploraClientTest extends BitcoinSAsyncTest {
|
||||
|
||||
val (site, proxyParams) = if (torEnabled) {
|
||||
val site = BlockstreamTorEsploraSite(MainNet)
|
||||
val proxy = torProxyAddress
|
||||
val params = Socks5ProxyParams(proxy, None, randomizeCredentials = true)
|
||||
(site, Some(params))
|
||||
} else {
|
||||
val site = MempoolSpaceEsploraSite(MainNet)
|
||||
(site, None)
|
||||
}
|
||||
|
||||
private val client =
|
||||
new EsploraClient(site, proxyParams)
|
||||
|
||||
it must "get a transaction details" in {
|
||||
val txId = DoubleSha256DigestBE(
|
||||
"7940246d561ecb869de19e91e306249eb0dfae84df43e59568b3f7092ce3190a")
|
||||
client
|
||||
.getTransaction(txId)
|
||||
.map { details =>
|
||||
assert(details.size == 226)
|
||||
assert(details.weight == 904)
|
||||
assert(details.version == Int32.two)
|
||||
assert(details.locktime == UInt32.zero)
|
||||
assert(details.txid == txId)
|
||||
assert(details.status.confirmed)
|
||||
assert(details.status.block_height.contains(720542))
|
||||
assert(details.status.block_time.contains(1643245253))
|
||||
assert(details.status.block_hash.contains(DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")))
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a transaction status" in {
|
||||
val txId = DoubleSha256DigestBE(
|
||||
"7940246d561ecb869de19e91e306249eb0dfae84df43e59568b3f7092ce3190a")
|
||||
client
|
||||
.getTransactionStatus(txId)
|
||||
.map { status =>
|
||||
assert(status.block_height.contains(720542))
|
||||
assert(status.block_time.contains(1643245253))
|
||||
assert(status.block_hash.contains(DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")))
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a raw transaction" in {
|
||||
val txId = DoubleSha256DigestBE(
|
||||
"7940246d561ecb869de19e91e306249eb0dfae84df43e59568b3f7092ce3190a")
|
||||
client
|
||||
.getRawTransaction(txId)
|
||||
.map { tx =>
|
||||
val expected = Transaction(
|
||||
"02000000017fc73ba05900f3b0eb2b3adb53b6899d2275a8f008fab03e01d6d5b4d8fd2aa8000000006b483045022100bb4bdad155b4f9431c279ef90fd2c7591fec8b194efa5fc022820906e1078b0f02201154bfd58ae73c44ea3df26c6ae7e41d9b39e2a2daa6fe9141c91db3fe37e8cb0121029da6c25d85a88c8a5f5982b1dacf88af00aba4239814015882238fd20cea022effffffff0222f20400000000001976a914b7122afa54297d62b3ff4666c2d07c4593f62bb588acdc030000000000001976a914e0fcab102a97ff6ffe8cd47e8e083da1918d0e0988ac00000000")
|
||||
|
||||
assert(tx == expected)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get address stats" in {
|
||||
val addr =
|
||||
BitcoinAddress.fromString("bc1qzd3y2pumhgtkrv5h2x96jaxgs056wvuwgqygac")
|
||||
|
||||
client.getAddressStats(addr).map { stats =>
|
||||
assert(stats.address == addr)
|
||||
assert(stats.chain_stats.spent_txo_count >= 2)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get address txs" in {
|
||||
val addr =
|
||||
BitcoinAddress.fromString("bc1qzd3y2pumhgtkrv5h2x96jaxgs056wvuwgqygac")
|
||||
|
||||
client.getAddressTxs(addr).map { txs =>
|
||||
assert(txs.size >= 3)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get address txs with last seen txid" in {
|
||||
val addr =
|
||||
BitcoinAddress.fromString("bc1qzd3y2pumhgtkrv5h2x96jaxgs056wvuwgqygac")
|
||||
|
||||
val txid = DoubleSha256DigestBE(
|
||||
"d4cbc49e9d6d051c5e23b843d06b5a187b506d238588e94bdb488f3846324bdc")
|
||||
|
||||
client.getAddressTxs(addr, txid).map { txs =>
|
||||
assert(txs.nonEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get mempool txs" in {
|
||||
val publicKey = ECPublicKey.freshPublicKey
|
||||
val spk = P2WPKHWitnessSPKV0(publicKey)
|
||||
val addr = BitcoinAddress.fromScriptPubKey(spk, MainNet)
|
||||
|
||||
client.getMempoolTxs(addr).map { txs =>
|
||||
// newly generated address should be empty
|
||||
assert(txs.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a block" in {
|
||||
val hash = DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")
|
||||
|
||||
client.getBlock(hash).map { block =>
|
||||
assert(block.id == hash)
|
||||
assert(block.height == 720542)
|
||||
assert(block.size == 327817)
|
||||
assert(block.weight == 784117)
|
||||
assert(block.version == Int32(834920448))
|
||||
assert(block.bits == UInt32(386568320))
|
||||
assert(block.nonce == UInt32(1808609338))
|
||||
assert(block.mediantime == UInt32(1643243095))
|
||||
assert(block.tx_count == 420)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a block header" in {
|
||||
val hash = DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")
|
||||
|
||||
client.getBlockHeader(hash).map { header =>
|
||||
assert(header.hashBE == hash)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a raw block" in {
|
||||
val hash = DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")
|
||||
|
||||
client.getRawBlock(hash).map { block =>
|
||||
assert(block.blockHeader.hashBE == hash)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a block's height " in {
|
||||
val expected = DoubleSha256DigestBE(
|
||||
"000000000000000000074110fd51c9e34b9ea10ea88ce7fa43bf2cf80a3c2185")
|
||||
|
||||
client.getBlockHashAtHeight(720542).map { hash =>
|
||||
assert(hash == expected)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get a block height " in {
|
||||
client.getBestHashBlockHeight().map { height =>
|
||||
assert(height > 720542)
|
||||
}
|
||||
}
|
||||
}
|
227
esplora/src/main/scala/org/bitcoins/esplora/EsploraClient.scala
Normal file
227
esplora/src/main/scala/org/bitcoins/esplora/EsploraClient.scala
Normal file
@ -0,0 +1,227 @@
|
||||
package org.bitcoins.esplora
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl._
|
||||
import akka.http.scaladsl.client.RequestBuilding.Post
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.util.ByteString
|
||||
import org.bitcoins.core.api.chain.ChainQueryApi
|
||||
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader}
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.crypto._
|
||||
import org.bitcoins.esplora.EsploraJsonModels._
|
||||
import org.bitcoins.tor._
|
||||
import play.api.libs.json._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.net.URI
|
||||
import scala.concurrent._
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
class EsploraClient(site: EsploraSite, proxyParams: Option[Socks5ProxyParams])(
|
||||
implicit system: ActorSystem)
|
||||
extends ChainQueryApi {
|
||||
implicit val ec: ExecutionContext = system.dispatcher
|
||||
|
||||
require(!site.isTor || site.isTor && proxyParams.isDefined,
|
||||
"proxyParams must be defined for a tor esplora site")
|
||||
|
||||
private val baseUrl = site.url
|
||||
|
||||
private val httpClient: HttpExt = Http(system)
|
||||
|
||||
private val httpConnectionPoolSettings =
|
||||
Socks5ClientTransport.createConnectionPoolSettings(new URI(baseUrl),
|
||||
proxyParams)
|
||||
|
||||
private def sendRawRequest(request: HttpRequest): Future[ByteString] = {
|
||||
httpClient
|
||||
.singleRequest(request, settings = httpConnectionPoolSettings)
|
||||
.flatMap(response =>
|
||||
response.entity.dataBytes
|
||||
.runFold(ByteString.empty)(_ ++ _))
|
||||
}
|
||||
|
||||
private def sendRequest(request: HttpRequest): Future[String] = {
|
||||
sendRawRequest(request)
|
||||
.map(payload => payload.decodeString(ByteString.UTF_8))
|
||||
}
|
||||
|
||||
private def sendRequestAndParse[T](httpRequest: HttpRequest)(implicit
|
||||
reads: Reads[T]): Future[T] = {
|
||||
sendRequest(httpRequest).map { str =>
|
||||
val json = Json.parse(str)
|
||||
json.validate[T] match {
|
||||
case JsSuccess(value, _) => value
|
||||
case JsError(errors) =>
|
||||
throw new RuntimeException(
|
||||
s"Error parsing json $json, ${errors.mkString("\n")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getTransaction(txId: DoubleSha256DigestBE): Future[EsploraTransaction] = {
|
||||
val url = baseUrl + s"/tx/${txId.hex}"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[EsploraTransaction](request)
|
||||
}
|
||||
|
||||
def getTransactionStatus(
|
||||
txId: DoubleSha256DigestBE): Future[EsploraTransactionStatus] = {
|
||||
val url = baseUrl + s"/tx/${txId.hex}/status"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[EsploraTransactionStatus](request)
|
||||
}
|
||||
|
||||
def getRawTransaction(txId: DoubleSha256DigestBE): Future[Transaction] = {
|
||||
val url = baseUrl + s"/tx/${txId.hex}/raw"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRawRequest(request).map { byteString =>
|
||||
val bytes = ByteVector(byteString.toArray)
|
||||
Transaction.fromBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
def broadcastTransaction(tx: Transaction): Future[DoubleSha256DigestBE] = {
|
||||
val url = baseUrl + "/tx"
|
||||
val request = Post(url, tx.hex)
|
||||
|
||||
sendRequest(request).map(DoubleSha256DigestBE.fromHex)
|
||||
}
|
||||
|
||||
def getAddressStats(addr: BitcoinAddress): Future[AddressStats] = {
|
||||
val url = baseUrl + s"/address/$addr"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[AddressStats](request)
|
||||
}
|
||||
|
||||
def getAddressTxs(
|
||||
addr: BitcoinAddress): Future[Vector[EsploraTransaction]] = {
|
||||
val url = baseUrl + s"/address/$addr/txs"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[Vector[EsploraTransaction]](request)
|
||||
}
|
||||
|
||||
def getAddressTxs(
|
||||
addr: BitcoinAddress,
|
||||
lastSeenTxId: DoubleSha256DigestBE): Future[
|
||||
Vector[EsploraTransaction]] = {
|
||||
val url = baseUrl + s"/address/$addr/txs/chain/${lastSeenTxId.hex}"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[Vector[EsploraTransaction]](request)
|
||||
}
|
||||
|
||||
def getMempoolTxs(
|
||||
addr: BitcoinAddress): Future[Vector[EsploraTransaction]] = {
|
||||
val url = baseUrl + s"/address/$addr/txs/mempool"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[Vector[EsploraTransaction]](request)
|
||||
}
|
||||
|
||||
def getBlock(hash: DoubleSha256DigestBE): Future[EsploraBlock] = {
|
||||
val url = baseUrl + s"/block/${hash.hex}"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequestAndParse[EsploraBlock](request)
|
||||
}
|
||||
|
||||
def getBlockHeader(hash: DoubleSha256DigestBE): Future[BlockHeader] = {
|
||||
val url = baseUrl + s"/block/${hash.hex}/header"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequest(request).map(BlockHeader.fromHex)
|
||||
}
|
||||
|
||||
def getBlockHashAtHeight(height: Int): Future[DoubleSha256DigestBE] = {
|
||||
val url = baseUrl + s"/block-height/$height"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequest(request).map(DoubleSha256DigestBE.fromHex)
|
||||
}
|
||||
|
||||
def getRawBlock(hash: DoubleSha256DigestBE): Future[Block] = {
|
||||
val url = baseUrl + s"/block/${hash.hex}/raw"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRawRequest(request).map { byteString =>
|
||||
val bytes = ByteVector(byteString.toArray)
|
||||
Block.fromBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// -- ChainQueryApi --
|
||||
override def getBestHashBlockHeight()(implicit
|
||||
_ec: ExecutionContext): Future[Int] = {
|
||||
val url = baseUrl + s"/blocks/tip/height"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequest(request).map(_.toInt)(ec)
|
||||
}
|
||||
|
||||
override def getBestBlockHash(): Future[DoubleSha256DigestBE] = {
|
||||
val url = baseUrl + s"/blocks/tip/hash"
|
||||
val request = HttpRequest(uri = url, method = HttpMethods.GET)
|
||||
|
||||
sendRequest(request).map(DoubleSha256DigestBE.fromHex)
|
||||
}
|
||||
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
|
||||
getBlock(blockHash)
|
||||
.map(_.height)
|
||||
.map(Some(_))
|
||||
.recover { case NonFatal(_) => None }
|
||||
}
|
||||
|
||||
override def getNumberOfConfirmations(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
|
||||
val tipF = getBestHashBlockHeight()
|
||||
val heightF = getBlockHeight(blockHash)
|
||||
|
||||
for {
|
||||
tip <- tipF
|
||||
heightOpt <- heightF
|
||||
} yield heightOpt.map(height => tip - height + 1)
|
||||
}
|
||||
|
||||
override def getFilterCount(): Future[Int] = Future.failed(
|
||||
new UnsupportedOperationException("Esplora does not support block filters"))
|
||||
|
||||
override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = {
|
||||
blockStamp match {
|
||||
case blockHeight: BlockStamp.BlockHeight =>
|
||||
Future.successful(blockHeight.height)
|
||||
case blockHash: BlockStamp.BlockHash =>
|
||||
getBlock(blockHash.hash).map(_.height)
|
||||
case blockTime: BlockStamp.BlockTime =>
|
||||
Future.failed(
|
||||
new UnsupportedOperationException(
|
||||
s"Not implemented for Esplora Client: $blockTime"))
|
||||
}
|
||||
}
|
||||
|
||||
override def getFiltersBetweenHeights(
|
||||
startHeight: Int,
|
||||
endHeight: Int): Future[Vector[ChainQueryApi.FilterResponse]] =
|
||||
Future.failed(
|
||||
new UnsupportedOperationException(
|
||||
"Esplora does not support block filters"))
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
Future.successful(0)
|
||||
|
||||
override def getMedianTimePast(): Future[Long] = {
|
||||
for {
|
||||
hash <- getBestBlockHash()
|
||||
block <- getBlock(hash)
|
||||
} yield block.mediantime.toLong
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package org.bitcoins.esplora
|
||||
|
||||
import org.bitcoins.commons.serializers.JsonSerializers._
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol._
|
||||
import org.bitcoins.crypto._
|
||||
import play.api.libs.json._
|
||||
|
||||
//import org.bitcoins.commons.serializers.JsonSerializers._
|
||||
|
||||
sealed abstract class EsploraJsonModel
|
||||
|
||||
object EsploraJsonModels {
|
||||
|
||||
case class EsploraTransactionStatus(
|
||||
confirmed: Boolean,
|
||||
block_height: Option[Long],
|
||||
block_hash: Option[DoubleSha256DigestBE],
|
||||
block_time: Option[Long])
|
||||
extends EsploraJsonModel
|
||||
|
||||
implicit val EsploraTransactionStatusReads: Reads[EsploraTransactionStatus] =
|
||||
Json.reads[EsploraTransactionStatus]
|
||||
|
||||
case class EsploraTransaction(
|
||||
txid: DoubleSha256DigestBE,
|
||||
version: Int32,
|
||||
locktime: UInt32,
|
||||
size: Int,
|
||||
weight: Int,
|
||||
fee: Satoshis,
|
||||
status: EsploraTransactionStatus)
|
||||
extends EsploraJsonModel
|
||||
|
||||
implicit val EsploraTransactionReads: Reads[EsploraTransaction] =
|
||||
Json.reads[EsploraTransaction]
|
||||
|
||||
case class AddressChainStats(
|
||||
funded_txo_count: Int,
|
||||
funded_txo_sum: Satoshis,
|
||||
spent_txo_count: Int,
|
||||
spent_txo_sum: Satoshis)
|
||||
extends EsploraJsonModel
|
||||
|
||||
implicit val addressChainStatsReads: Reads[AddressChainStats] =
|
||||
Json.reads[AddressChainStats]
|
||||
|
||||
case class AddressStats(
|
||||
address: BitcoinAddress,
|
||||
chain_stats: AddressChainStats,
|
||||
mempool_stats: AddressChainStats)
|
||||
extends EsploraJsonModel {
|
||||
|
||||
val totalReceived: CurrencyUnit =
|
||||
chain_stats.funded_txo_sum + mempool_stats.funded_txo_sum
|
||||
|
||||
val totalSpent: CurrencyUnit =
|
||||
chain_stats.spent_txo_sum + mempool_stats.spent_txo_sum
|
||||
|
||||
val balance: CurrencyUnit = totalReceived - totalSpent
|
||||
}
|
||||
|
||||
implicit val addressStatsReads: Reads[AddressStats] =
|
||||
Json.reads[AddressStats]
|
||||
|
||||
case class EsploraBlock(
|
||||
id: DoubleSha256DigestBE,
|
||||
height: Int,
|
||||
version: Int32,
|
||||
timestamp: UInt32,
|
||||
mediantime: UInt32,
|
||||
bits: UInt32,
|
||||
nonce: UInt32,
|
||||
merkle_root: DoubleSha256DigestBE,
|
||||
tx_count: Int,
|
||||
size: Int,
|
||||
weight: Int,
|
||||
previousblockhash: DoubleSha256DigestBE
|
||||
) extends EsploraJsonModel
|
||||
|
||||
implicit val EsploraBlockReads: Reads[EsploraBlock] =
|
||||
Json.reads[EsploraBlock]
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package org.bitcoins.esplora
|
||||
|
||||
import org.bitcoins.core.config._
|
||||
|
||||
sealed abstract class EsploraSite {
|
||||
def url: String
|
||||
def isTor: Boolean
|
||||
}
|
||||
|
||||
case class BlockstreamEsploraSite(network: BitcoinNetwork) extends EsploraSite {
|
||||
|
||||
override val url: String = network match {
|
||||
case MainNet => "https://blockstream.info/api"
|
||||
case TestNet3 => "https://blockstream.info/testnet/api"
|
||||
case net @ (RegTest | SigNet) =>
|
||||
sys.error(s"Blockstream.info does not support $net")
|
||||
}
|
||||
|
||||
override val isTor: Boolean = false
|
||||
}
|
||||
|
||||
case class BlockstreamTorEsploraSite(network: BitcoinNetwork)
|
||||
extends EsploraSite {
|
||||
|
||||
override val url: String = network match {
|
||||
case MainNet =>
|
||||
"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api"
|
||||
case TestNet3 =>
|
||||
"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api"
|
||||
case net @ (RegTest | SigNet) =>
|
||||
sys.error(s"Blockstream.info does not support $net")
|
||||
}
|
||||
|
||||
override val isTor: Boolean = true
|
||||
}
|
||||
|
||||
case class MempoolSpaceEsploraSite(network: BitcoinNetwork)
|
||||
extends EsploraSite {
|
||||
|
||||
override val url: String = network match {
|
||||
case MainNet => "https://mempool.space/api"
|
||||
case TestNet3 => "https://mempool.space/testnet/api"
|
||||
case SigNet => "https://mempool.space/signet/api"
|
||||
case RegTest =>
|
||||
sys.error(s"Mempool.space cannot be used for RegTest")
|
||||
}
|
||||
|
||||
override val isTor: Boolean = false
|
||||
}
|
||||
|
||||
case class MempoolSpaceTorEsploraSite(network: BitcoinNetwork)
|
||||
extends EsploraSite {
|
||||
|
||||
override val url: String = network match {
|
||||
case MainNet =>
|
||||
"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api"
|
||||
case TestNet3 =>
|
||||
"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api"
|
||||
case SigNet =>
|
||||
"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/signet/api"
|
||||
case RegTest =>
|
||||
sys.error(s"Mempool.space cannot be used for RegTest")
|
||||
}
|
||||
|
||||
override val isTor: Boolean = true
|
||||
}
|
||||
|
||||
case class CustomEsploraSite(baseUrl: String, isTor: Boolean)
|
||||
extends EsploraSite {
|
||||
|
||||
override val url: String = {
|
||||
if (baseUrl.endsWith("/")) baseUrl.init
|
||||
else baseUrl
|
||||
}
|
||||
}
|
@ -538,6 +538,21 @@ object Deps {
|
||||
)
|
||||
}
|
||||
|
||||
val esplora = Def.setting {
|
||||
List(
|
||||
Compile.akkaHttp,
|
||||
Compile.akkaActor,
|
||||
Compile.akkaStream
|
||||
)
|
||||
}
|
||||
|
||||
val esploraTest = Def.setting {
|
||||
List(
|
||||
Test.akkaTestkit,
|
||||
Test.scalaTest.value
|
||||
)
|
||||
}
|
||||
|
||||
val node = List(
|
||||
Compile.akkaActor,
|
||||
Compile.logback,
|
||||
|
Loading…
Reference in New Issue
Block a user