solution: track Bitcoin transactions
This commit is contained in:
@@ -31,7 +31,7 @@ import reactor.core.publisher.Mono
|
||||
class BlockchainRpc(
|
||||
@Autowired private val nativeCall: NativeCall,
|
||||
@Autowired private val streamHead: StreamHead,
|
||||
@Autowired private val trackEthereumTx: TrackEthereumTx,
|
||||
@Autowired private val trackTx: List<TrackTx>,
|
||||
@Autowired private val trackAddress: List<TrackAddress>,
|
||||
@Autowired private val describe: Describe,
|
||||
@Autowired private val subscribeStatus: SubscribeStatus
|
||||
@@ -48,7 +48,11 @@ class BlockchainRpc(
|
||||
}
|
||||
|
||||
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
|
||||
return trackEthereumTx.add(request)
|
||||
return request.flatMapMany { request ->
|
||||
val chain = Chain.byId(request.chainValue)
|
||||
trackTx.find { it.isSupported(chain) }?.subscribe(request)
|
||||
?: Flux.error(SilentException.UnsupportedBlockchain(chain))
|
||||
}
|
||||
}
|
||||
|
||||
override fun subscribeBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
||||
|
||||
158
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt
Normal file
158
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt
Normal file
@@ -0,0 +1,158 @@
|
||||
package io.emeraldpay.dshackle.rpc
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.math.BigInteger
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Service
|
||||
class TrackBitcoinTx(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
) : TrackTx {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TrackBitcoinTx::class.java)
|
||||
}
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain)
|
||||
}
|
||||
|
||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||
val chain = Chain.byId(request.chainValue)
|
||||
val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java)
|
||||
?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
|
||||
val txid = request.txId
|
||||
val confirmations = max(min(1, request.confirmationLimit), 12)
|
||||
return upstream.getApi(Selector.empty).flatMapMany { api ->
|
||||
subscribe(chain, api, upstream, txid)
|
||||
}.takeUntil { tx ->
|
||||
tx.confirmations >= confirmations
|
||||
}.map(this::asProto)
|
||||
}
|
||||
|
||||
fun subscribe(chain: Chain, api: BitcoinApi, upstream: Upstream<BitcoinApi>, txid: String): Flux<TxStatus> {
|
||||
return loadExisting(api, txid)
|
||||
.flatMapMany { status ->
|
||||
if (status.mined) {
|
||||
//Head almost always knows the current height, so it can continue with calculating confirmations
|
||||
//without publishing an empty TxStatus first
|
||||
continueWithMined(api, upstream, status)
|
||||
} else {
|
||||
loadMempool(api, txid)
|
||||
.flatMapMany { tx ->
|
||||
val next = if (tx.found) {
|
||||
untilMined(upstream, tx)
|
||||
} else {
|
||||
untilFound(chain, api, upstream, txid)
|
||||
}
|
||||
//fist provide the current status, then updates
|
||||
Flux.concat(Mono.just(tx), next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun continueWithMined(api: BitcoinApi, upstream: Upstream<BitcoinApi>, status: TxStatus): Flux<TxStatus> {
|
||||
return api.getBlock(status.blockHash!!)
|
||||
.map { block ->
|
||||
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
|
||||
}.flatMapMany { tx ->
|
||||
withConfirmations(upstream, tx)
|
||||
}
|
||||
}
|
||||
|
||||
fun untilFound(chain: Chain, api: BitcoinApi, upstream: Upstream<BitcoinApi>, txid: String): Flux<TxStatus> {
|
||||
return Flux.interval(Duration.ofSeconds(1))
|
||||
.take(Duration.ofMinutes(10))
|
||||
.flatMap { loadMempool(api, txid) }
|
||||
.skipUntil { it.found }
|
||||
.flatMap { subscribe(chain, api, upstream, txid) }
|
||||
.doOnError { t ->
|
||||
log.error("Failed to wait until found", t)
|
||||
}
|
||||
}
|
||||
|
||||
fun untilMined(upstream: Upstream<BitcoinApi>, tx: TxStatus): Mono<TxStatus> {
|
||||
return upstream.getHead().getFlux().flatMap {
|
||||
upstream.getApi(Selector.empty).flatMap { api ->
|
||||
loadExisting(api, tx.txid)
|
||||
}.filter { it.mined }
|
||||
}.single()
|
||||
}
|
||||
|
||||
fun withConfirmations(upstream: Upstream<BitcoinApi>, tx: TxStatus): Flux<TxStatus> {
|
||||
return upstream.getHead().getFlux().map {
|
||||
tx.withHead(it.height)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadExisting(api: BitcoinApi, txid: String): Mono<TxStatus> {
|
||||
val mined = api.getTx(txid)
|
||||
return mined.map {
|
||||
val block = it["blockhash"] as String?
|
||||
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMempool(api: BitcoinApi, txid: String): Mono<TxStatus> {
|
||||
val mempool = api.getMempool()
|
||||
return mempool.map {
|
||||
if (it.contains(txid)) {
|
||||
TxStatus(txid, found = true, mined = false)
|
||||
} else {
|
||||
TxStatus(txid, found = false, mined = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun asProto(tx: TxStatus): BlockchainOuterClass.TxStatus {
|
||||
val data = BlockchainOuterClass.TxStatus.newBuilder()
|
||||
.setTxId(tx.txid)
|
||||
.setConfirmations(tx.confirmations.toInt())
|
||||
|
||||
data.broadcasted = tx.found
|
||||
val isMined = tx.mined
|
||||
data.mined = isMined
|
||||
if (isMined) {
|
||||
data.setBlock(
|
||||
Common.BlockInfo.newBuilder()
|
||||
.setBlockId(tx.blockHash!!.substring(2))
|
||||
.setTimestamp(tx.blockTime!!.toEpochMilli())
|
||||
.setWeight(ByteString.copyFrom(tx.blockTotalDifficulty!!.toByteArray()))
|
||||
.setHeight(tx.height!!)
|
||||
)
|
||||
}
|
||||
return data.build()
|
||||
}
|
||||
|
||||
class TxStatus(
|
||||
val txid: String,
|
||||
val found: Boolean = false,
|
||||
val height: Long? = null,
|
||||
val mined: Boolean = false,
|
||||
val blockHash: String? = null,
|
||||
val blockTime: Instant? = null,
|
||||
val blockTotalDifficulty: BigInteger? = null,
|
||||
val confirmations: Long = 0) {
|
||||
|
||||
fun withHead(headHeight: Long) = TxStatus(txid, found, height, mined, blockHash, blockTime, blockTotalDifficulty, headHeight - height!! + 1)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ import kotlin.math.min
|
||||
class TrackEthereumTx(
|
||||
@Autowired private val upstreams: Upstreams,
|
||||
@Autowired private val upstreamScheduler: Scheduler
|
||||
) {
|
||||
) : TrackTx {
|
||||
|
||||
companion object {
|
||||
private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000")
|
||||
@@ -75,6 +75,10 @@ class TrackEthereumTx(
|
||||
|
||||
val notFound = ConcurrentLinkedQueue<TrackedTx>()
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun init() {
|
||||
upstreams.observeChains().subscribe { chain ->
|
||||
@@ -165,7 +169,7 @@ class TrackEthereumTx(
|
||||
|
||||
fun streamAllUpdates(tx: TxDetails): Flux<BlockchainOuterClass.TxStatus> {
|
||||
val current = checkForUpdate(tx)
|
||||
.doOnNext (this::onFirstUpdate)
|
||||
.doOnNext(this::onFirstUpdate)
|
||||
.map { Notification(it, asProto(it)) }
|
||||
val updates = Flux.from(tx.bus)
|
||||
|
||||
@@ -175,12 +179,9 @@ class TrackEthereumTx(
|
||||
.map { it.proto }
|
||||
}
|
||||
|
||||
fun add(requestMono: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
|
||||
return requestMono.map { request ->
|
||||
prepareTracking(request)
|
||||
}.flatMapMany { tx ->
|
||||
streamAllUpdates(tx)
|
||||
}
|
||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||
val tx = prepareTracking(request)
|
||||
return streamAllUpdates(tx)
|
||||
}
|
||||
|
||||
private fun verifyAll(chain: Chain) {
|
||||
|
||||
10
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt
Normal file
10
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package io.emeraldpay.dshackle.rpc
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
interface TrackTx {
|
||||
fun isSupported(chain: Chain): Boolean
|
||||
fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus>
|
||||
}
|
||||
@@ -49,4 +49,5 @@ abstract class AbstractHead : Head {
|
||||
fun getCurrent(): BlockContainer? {
|
||||
return head.get()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface Head {
|
||||
fun getFlux(): Flux<BlockContainer>
|
||||
|
||||
@@ -41,4 +41,16 @@ open class BitcoinApi(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getBlock(hash: String): Mono<Map<String, Any>> {
|
||||
return executeAndResult(0, "getblock", listOf(hash), Map::class.java) as Mono<Map<String, Any>>
|
||||
}
|
||||
|
||||
open fun getTx(txid: String): Mono<Map<String, Any>> {
|
||||
return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono<Map<String, Any>>
|
||||
}
|
||||
|
||||
open fun getMempool(): Mono<List<String>> {
|
||||
return executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono<List<String>>
|
||||
}
|
||||
}
|
||||
@@ -15,26 +15,42 @@ class ExtractBlock(
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ExtractBlock::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun getHeight(data: Map<String, Any>): Long? {
|
||||
val height = data["height"] as Number? ?: return null
|
||||
return height.toLong()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getTime(data: Map<String, Any>): Instant? {
|
||||
val time = data["time"] as Number? ?: return null
|
||||
return Instant.ofEpochMilli(time.toLong() * 1000)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDifficulty(data: Map<String, Any>): BigInteger? {
|
||||
val chainwork = data["chainwork"] as String? ?: return null
|
||||
return BigInteger(1, Hex.decodeHex(chainwork))
|
||||
}
|
||||
}
|
||||
|
||||
fun extract(json: ByteArray): BlockContainer {
|
||||
val data = objectMapper.readValue(json, Map::class.java) as Map<String, Any>
|
||||
|
||||
val height = data["height"] as Number? ?: throw IllegalArgumentException("Block JSON has no height")
|
||||
val time = data["time"] as Number? ?: throw IllegalArgumentException("Block JSON has no time")
|
||||
val hash = data["hash"] as String? ?: throw IllegalArgumentException("Block JSON has no hash")
|
||||
val chainwork = data["chainwork"] as String? ?: throw IllegalArgumentException("Block JSON has no chainwork")
|
||||
val transactions = (data["tx"] as List<String>?)?.map(TxId.Companion::from) ?: emptyList()
|
||||
|
||||
return BlockContainer(
|
||||
height.toLong(),
|
||||
getHeight(data) ?: throw IllegalArgumentException("Block JSON has no height"),
|
||||
BlockId.from(hash),
|
||||
BigInteger(1, Hex.decodeHex(chainwork)),
|
||||
Instant.ofEpochMilli(time.toLong() * 1000),
|
||||
getDifficulty(data) ?: throw IllegalArgumentException("Block JSON has no chainwork"),
|
||||
getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
|
||||
false,
|
||||
json,
|
||||
transactions
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user