solution: naive implementation for tx tracking
This commit is contained in:
@@ -5,13 +5,16 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class BlockchainRpc(
|
class BlockchainRpc(
|
||||||
@Autowired private val nativeCall: NativeCall,
|
@Autowired private val nativeCall: NativeCall,
|
||||||
@Autowired private val streamHead: StreamHead
|
@Autowired private val streamHead: StreamHead,
|
||||||
|
@Autowired private val trackTx: TrackTx
|
||||||
): BlockchainGrpc.BlockchainImplBase() {
|
): BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
|
||||||
override fun nativeCall(request: BlockchainOuterClass.CallBlockchainRequest, responseObserver: StreamObserver<BlockchainOuterClass.CallBlockchainReplyItem>) {
|
override fun nativeCall(request: BlockchainOuterClass.CallBlockchainRequest, responseObserver: StreamObserver<BlockchainOuterClass.CallBlockchainReplyItem>) {
|
||||||
@@ -21,4 +24,15 @@ class BlockchainRpc(
|
|||||||
override fun streamHead(request: Common.Chain, responseObserver: StreamObserver<BlockchainOuterClass.ChainHead>) {
|
override fun streamHead(request: Common.Chain, responseObserver: StreamObserver<BlockchainOuterClass.ChainHead>) {
|
||||||
streamHead.add(Chain.byId(request.type.number), responseObserver)
|
streamHead.add(Chain.byId(request.type.number), responseObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun trackTx(request: BlockchainOuterClass.TrackTxRequest, responseObserver: StreamObserver<BlockchainOuterClass.TxStatus>) {
|
||||||
|
val tx = TrackTx.TrackedTx(
|
||||||
|
Chain.byId(request.chainValue),
|
||||||
|
StreamSender(responseObserver),
|
||||||
|
Instant.now(),
|
||||||
|
TransactionId.from(request.txid),
|
||||||
|
Math.min(Math.max(1, request.confirmations), 100)
|
||||||
|
)
|
||||||
|
trackTx.add(tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
180
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt
Normal file
180
src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
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.upstream.Upstreams
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
|
import io.infinitape.etherjar.rpc.Batch
|
||||||
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.toFlux
|
||||||
|
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TrackTx(
|
||||||
|
@Autowired private val upstreams: Upstreams
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(TrackTx::class.java)
|
||||||
|
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedTx>>()
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun init() {
|
||||||
|
listOf(Chain.MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM).forEach { chain ->
|
||||||
|
clients[chain] = ConcurrentLinkedQueue()
|
||||||
|
upstreams.ethereumUpstream(chain)?.head?.let { head ->
|
||||||
|
head.getFlux().subscribe { verifyAll(chain) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentList(chain: Chain): ConcurrentLinkedQueue<TrackedTx> {
|
||||||
|
return clients[chain]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(tx: TrackedTx) {
|
||||||
|
currentList(tx.chain).add(tx)
|
||||||
|
verify(tx)
|
||||||
|
notify(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyAll(chain: Chain) {
|
||||||
|
currentList(chain)
|
||||||
|
.toFlux()
|
||||||
|
.filter(this::verify)
|
||||||
|
.subscribe {
|
||||||
|
notify(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verify(tx: TrackedTx): Boolean {
|
||||||
|
val found = tx.status.found
|
||||||
|
val mined = tx.status.mined
|
||||||
|
val batch = Batch()
|
||||||
|
val execution = Mono.fromCompletionStage(batch.add(Commands.eth().getTransaction(tx.txid)))
|
||||||
|
val upstream = upstreams.ethereumUpstream(tx.chain)!!
|
||||||
|
upstream.api.execute(batch)
|
||||||
|
val update = execution.flatMap {
|
||||||
|
if (it.blockNumber != null
|
||||||
|
&& it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
||||||
|
tx.withStatus(
|
||||||
|
blockHash = it.blockHash,
|
||||||
|
height = it.blockNumber,
|
||||||
|
found = true,
|
||||||
|
mined = true,
|
||||||
|
confirmation = 1
|
||||||
|
)
|
||||||
|
return@flatMap upstream.head.getHead().map { head ->
|
||||||
|
tx.withStatus(
|
||||||
|
confirmation = head.number - tx.status.height!! + 1,
|
||||||
|
blockTime = head.timestamp.toInstant()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tx.withStatus(
|
||||||
|
found = true,
|
||||||
|
mined = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@flatMap Mono.just(tx)
|
||||||
|
}.block()
|
||||||
|
if (update == null) {
|
||||||
|
tx.withStatus(
|
||||||
|
found = false,
|
||||||
|
mined = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return tx.status.found != found
|
||||||
|
}
|
||||||
|
if (!mined) {
|
||||||
|
return tx.status.mined != mined
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(tx: TrackedTx): Boolean {
|
||||||
|
val client = tx.stream
|
||||||
|
val data = BlockchainOuterClass.TxStatus.newBuilder()
|
||||||
|
.setChainValue(tx.chain.id)
|
||||||
|
.setTxid(tx.txid.toHex())
|
||||||
|
.setConfirmations(tx.status.confirmation.toInt())
|
||||||
|
.setMined(tx.status.mined)
|
||||||
|
.setBroadcasted(tx.status.found)
|
||||||
|
|
||||||
|
if (tx.status.mined) {
|
||||||
|
data.setBlock(
|
||||||
|
Common.BlockInfo.newBuilder()
|
||||||
|
.setHash(ByteString.copyFrom(tx.status.blockHash!!.bytes))
|
||||||
|
.setHeight(tx.status.height!!)
|
||||||
|
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var sent: Boolean = false
|
||||||
|
try {
|
||||||
|
sent = client.send(data.build())
|
||||||
|
if (!sent || tx.shouldClose()) {
|
||||||
|
if (sent) {
|
||||||
|
client.stream.onCompleted()
|
||||||
|
}
|
||||||
|
currentList(tx.chain).remove(tx)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Send error ${e.javaClass}: ${e.message}")
|
||||||
|
}
|
||||||
|
return sent
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackedTx(val chain: Chain,
|
||||||
|
val stream: StreamSender<BlockchainOuterClass.TxStatus>,
|
||||||
|
val since: Instant,
|
||||||
|
val txid: TransactionId,
|
||||||
|
val maxConfirmations: Int,
|
||||||
|
var status: TxStatus = TxStatus()) {
|
||||||
|
|
||||||
|
fun withStatus(found: Boolean = this.status.found,
|
||||||
|
height: Long? = this.status.height,
|
||||||
|
mined: Boolean = this.status.mined,
|
||||||
|
blockHash: BlockHash? = this.status.blockHash,
|
||||||
|
blockTime: Instant? = this.status.blockTime,
|
||||||
|
confirmation: Long = this.status.confirmation): TrackedTx {
|
||||||
|
this.status = this.status.copy(found, height, mined, blockHash, blockTime, confirmation)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldClose(): Boolean {
|
||||||
|
return maxConfirmations <= this.status.confirmation
|
||||||
|
|| since.isBefore(Instant.now().minus(Duration.ofHours(1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TxStatus(var found: Boolean = false,
|
||||||
|
var height: Long? = null,
|
||||||
|
var mined: Boolean = false,
|
||||||
|
var blockHash: BlockHash? = null,
|
||||||
|
var blockTime: Instant? = null,
|
||||||
|
var confirmation: Long = 0) {
|
||||||
|
fun copy(found: Boolean = this.found,
|
||||||
|
height: Long? = this.height,
|
||||||
|
mined: Boolean = this.mined,
|
||||||
|
blockHash: BlockHash? = this.blockHash,
|
||||||
|
blockTime: Instant? = this.blockTime,
|
||||||
|
confirmation: Long = this.confirmation)
|
||||||
|
= TxStatus(found, height, mined, blockHash, blockTime, confirmation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user