problem: Ethereum Tx tracking has complex non-reactive logic

This commit is contained in:
Igor Artamonov
2020-04-30 23:42:51 -04:00
parent 605c817149
commit 549b8b1b5d
2 changed files with 221 additions and 312 deletions

View File

@@ -21,6 +21,8 @@ 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.data.BlockContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
@@ -36,169 +38,150 @@ import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import reactor.core.scheduler.Scheduler
import reactor.util.function.Tuples
import reactor.core.scheduler.Schedulers
import reactor.util.retry.Retry
import java.math.BigInteger
import java.time.Duration
import java.time.Instant
import java.time.Period
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicLong
import javax.annotation.PostConstruct
import kotlin.collections.HashMap
import kotlin.math.max
import kotlin.math.min
@Service
class TrackEthereumTx(
@Autowired private val upstreams: Upstreams,
@Autowired private val upstreamScheduler: Scheduler
@Autowired private val upstreams: Upstreams
) : TrackTx {
companion object {
private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000")
private val FRESH_TX = Duration.ofSeconds(60)
private val TRACK_TTL = Duration.ofHours(1)
private val NOT_FOUND_TRACK_TTL = Duration.ofMinutes(1)
private val NOT_MINED_TRACK_TTL = NOT_FOUND_TRACK_TTL.multipliedBy(2)
private val PING_PERIOD = Duration.ofMinutes(5)
}
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedTx>>()
private val seq = AtomicLong(0)
var scheduler: Scheduler = Schedulers.elastic()
val notFound = ConcurrentLinkedQueue<TrackedTx>()
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
}
@PostConstruct
fun init() {
upstreams.observeChains().subscribe { chain ->
clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) }
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val base = prepareTracking(request)
val up = upstreams.getUpstream(base.chain)?.castApi(EthereumApi::class.java)
?: return Flux.empty()
return update(base)
.defaultIfEmpty(base)
.flatMapMany {
Flux.concat(Mono.just(it), subscribe(it, up))
.distinctUntilChanged(TxDetails::status)
.map(this@TrackEthereumTx::asProto)
.subscribeOn(scheduler)
}
}
fun subscribe(base: TxDetails, up: Upstream<EthereumApi>): Flux<TxDetails> {
var latestTx = base
val untilFound = Mono.just(latestTx)
.subscribeOn(scheduler)
.map {
//replace with the latest value, it may be already found
latestTx
}
.flatMap { latest ->
if (!latest.status.found) {
update(latest).defaultIfEmpty(latestTx)
} else {
Mono.just(latest)
}
}
.flatMap { received ->
if (!received.status.found) {
Mono.error(SilentException("Retry not found"))
} else {
Mono.just(received)
}
}
.retryWhen(
Retry.fixedDelay(10, Duration.ofSeconds(2))
)
.onErrorResume { Mono.empty() }
val inBlocks = up.getHead().getFlux()
.subscribeOn(scheduler)
.flatMap { block ->
onNewBlock(latestTx, block)
}
return Flux.merge(untilFound, inBlocks)
.takeUntil(TxDetails::shouldClose)
.doOnNext { newTx ->
latestTx = newTx
}
}
fun onNewBlock(tx: TxDetails, block: BlockContainer): Mono<TxDetails> {
val txid = TxId.from(tx.txid)
if (!tx.status.mined) {
val justMined = block.transactions.contains(txid)
return if (justMined) {
Mono.just(tx.withStatus(
mined = true,
found = true,
confirmations = 1,
height = block.height,
blockTime = block.timestamp,
blockTotalDifficulty = block.difficulty,
blockHash = BlockHash(block.hash.value)
))
} else {
update(tx)
}
} else {
//verify if it's still on chain
//TODO head is supposed to erase block when it was replaced, so can safely recalc here
return update(tx)
}
}
@Scheduled(fixedDelay = 5000)
fun recheckFreshNotFound() {
val recent = Instant.now().minus(FRESH_TX)
verifyAll(
notFound.filter {
val tx = it.tx
!tx.status.found && tx.since.isAfter(recent)
}.map { it.tx }
)
}
@Scheduled(fixedRate = 60000)
fun recheckMatureNotFound() {
val recent = Instant.now().minus(FRESH_TX)
verifyAll(
notFound.filter {
!it.tx.status.found && it.tx.since.isBefore(recent)
}.map { it.tx }
)
}
fun recheckNotFound() {
verifyAll(notFound.map { it.tx })
}
@Scheduled(fixedRate = 60000, initialDelay = 10000)
fun cleanupNotFound() {
notFound.removeIf {
it.tx.shouldClose() || it.tx.status.found
}
}
private fun trackedForChain(chain: Chain): List<TxDetails>? {
return clients[chain]?.map { it.tx }
}
fun onFirstUpdate(tx: TxDetails) {
tx.backref?.let { backref ->
clients[tx.chain]?.add(backref)
if (!tx.status.found) {
notFound.add(backref)
}
}
}
fun onFinished(chain: Chain, txid: Long) {
clients[chain]?.removeIf { x -> x.tx.id == txid }
notFound.removeIf { x -> x.tx.id == txid }
}
fun onSend(tx: TxDetails) {
val curr = tx.justNotified().makeCurrent()
if (curr.shouldClose()) {
curr.bus.onComplete()
}
private fun update(tx: TxDetails): Mono<TxDetails> {
val initialStatus = tx.status
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi>?
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
val execution = upstream.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getTransaction(tx.txid)) }
return execution
.onErrorResume(RpcException::class.java) { t ->
log.warn("Upstream error, ignoring. {}", t.rpcMessage)
Mono.empty<TransactionJson>()
}
.flatMap { updateFromBlock(upstream, tx, it) }
.doOnError { t ->
log.error("Failed to load tx block", t)
}
.switchIfEmpty(Mono.just(tx.withStatus(found = false)))
.filter { current ->
initialStatus != current.status || current.shouldClose()
}
}
fun prepareTracking(request: BlockchainOuterClass.TxStatusRequest): TxDetails {
val chain = Chain.byId(request.chainValue)
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
if (!isSupported(chain)) {
throw SilentException.UnsupportedBlockchain(request.chainValue)
}
if (!clients.containsKey(chain)) {
throw SilentException.UnsupportedBlockchain(chain)
}
val bus = TopicProcessor.create<Notification>()
val details = TxDetails(
chain,
bus,
Instant.now(),
TransactionId.from(request.txId),
min(max(1, request.confirmationLimit), 100),
seq.incrementAndGet()
min(max(1, request.confirmationLimit), 100)
)
val tracked = TrackedTx(details)
return details.withBackref(tracked)
}
fun streamAllUpdates(tx: TxDetails): Flux<BlockchainOuterClass.TxStatus> {
val current = checkForUpdate(tx)
.doOnNext(this::onFirstUpdate)
.map { Notification(it, asProto(it)) }
val updates = Flux.from(tx.bus)
return Flux.concat(current, updates)
.doFinally { onFinished(tx.chain, tx.id) }
.doOnNext { onSend(it.tx) }
.map { it.proto }
}
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val tx = prepareTracking(request)
return streamAllUpdates(tx)
}
private fun verifyAll(chain: Chain) {
verifyAll(trackedForChain(chain)!!)
}
private fun verifyAll(list: Collection<TxDetails>) {
list
.toFlux()
.parallel(8).runOn(upstreamScheduler)
.flatMap { checkForUpdate(it) }
.sequential()
.map { Tuples.of(it, asProto(it)) }
.subscribe { t ->
notify(t.t1.bus, t.t2, t.t1)
}
return details
}
fun setBlockDetails(tx: TxDetails, block: BlockJson<TransactionRefJson>): TxDetails {
@@ -255,27 +238,6 @@ class TrackEthereumTx(
}
}
private fun checkForUpdate(tx: TxDetails): Mono<TxDetails> {
val initialStatus = tx.status
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi>?
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
val execution = upstream.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getTransaction(tx.txid)) }
return execution
.onErrorResume(RpcException::class.java) { t ->
log.warn("Upstream error, ignoring. {}", t.rpcMessage)
Mono.empty<TransactionJson>()
}
.flatMap { updateFromBlock(upstream, tx, it) }
.doOnError { t ->
log.error("Failed to load tx block", t)
}
.switchIfEmpty(Mono.just(tx.withStatus(found = false)))
.filter { current ->
initialStatus != current.status || current.shouldNotify() || current.shouldClose()
}
}
private fun asProto(tx: TxDetails): BlockchainOuterClass.TxStatus {
val data = BlockchainOuterClass.TxStatus.newBuilder()
.setTxId(tx.txid.toHex())
@@ -296,35 +258,22 @@ class TrackEthereumTx(
return data.build()
}
private fun notify(client: TopicProcessor<Notification>, data: BlockchainOuterClass.TxStatus, tx: TxDetails) {
try {
client.onNext(Notification(tx, data))
} catch (t: Throwable) {
log.warn("Failed to put to bus", t)
}
}
class Notification(val tx: TxDetails, val proto: BlockchainOuterClass.TxStatus)
class TrackedTx(var tx: TxDetails)
class TxDetails(val chain: Chain,
val bus: TopicProcessor<Notification>,
val since: Instant,
val txid: TransactionId,
val maxConfirmations: Int,
val id: Long,
val backref: TrackedTx? = null,
val status: TxStatus = TxStatus(),
val notifiedAt: Instant = Instant.now().minus(Period.ofDays(1))
val status: TxStatus
) {
constructor(chain: Chain,
since: Instant,
txid: TransactionId,
maxConfirmations: Int) : this(chain, since, txid, maxConfirmations, TxStatus())
fun copy(
since: Instant = this.since,
backref: TrackedTx? = this.backref,
status: TxStatus = this.status,
notifiedAt: Instant = this.notifiedAt
) = TxDetails(chain, bus, since, txid, maxConfirmations, id, backref, status, notifiedAt)
status: TxStatus = this.status
) = TxDetails(chain, since, txid, maxConfirmations, status)
fun withStatus(found: Boolean = this.status.found,
height: Long? = this.status.height,
@@ -336,10 +285,6 @@ class TrackEthereumTx(
return copy(status = this.status.copy(found, height, mined, blockHash, blockTime, blockTotalDifficulty, confirmations))
}
fun withCleanStatus(): TxDetails {
return copy(status = this.status.clean())
}
fun shouldClose(): Boolean {
return maxConfirmations <= this.status.confirmations
|| since.isBefore(Instant.now().minus(TRACK_TTL))
@@ -347,26 +292,31 @@ class TrackEthereumTx(
|| (!status.mined && since.isBefore(Instant.now().minus(NOT_MINED_TRACK_TTL)))
}
fun shouldNotify(): Boolean {
return this.notifiedAt.isBefore(Instant.now().minus(PING_PERIOD))
override fun toString(): String {
return "TxDetails(chain=$chain, txid=$txid, status=$status)"
}
fun justNotified(): TxDetails {
return notifiedAt(Instant.now())
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TxDetails) return false
if (chain != other.chain) return false
if (since != other.since) return false
if (txid != other.txid) return false
if (maxConfirmations != other.maxConfirmations) return false
if (status != other.status) return false
return true
}
fun notifiedAt(time: Instant): TxDetails {
return copy(notifiedAt = time)
override fun hashCode(): Int {
var result = chain.hashCode()
result = 31 * result + since.hashCode()
result = 31 * result + txid.hashCode()
result = 31 * result + status.hashCode()
return result
}
fun withBackref(backref: TrackedTx): TxDetails {
return copy(backref = backref)
}
fun makeCurrent(): TxDetails {
backref?.tx = this
return this
}
}
class TxStatus(val found: Boolean = false,
@@ -412,5 +362,10 @@ class TrackEthereumTx(
return result
}
override fun toString(): String {
return "TxStatus(found=$found, height=$height, mined=$mined, blockHash=$blockHash, blockTime=$blockTime, blockTotalDifficulty=$blockTotalDifficulty, confirmations=$confirmations)"
}
}
}

View File

@@ -20,9 +20,15 @@ import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
@@ -30,9 +36,10 @@ import io.infinitape.etherjar.rpc.ReactorRpcClient
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import reactor.test.scheduler.VirtualTimeScheduler
import spock.lang.Ignore
import spock.lang.Specification
import java.time.Duration
@@ -96,8 +103,7 @@ class TrackEthereumTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
apiMock.answer("eth_getBlockByHash", [blockJson.hash.toHex(), false], blockJson)
@@ -112,54 +118,36 @@ class TrackEthereumTxSpec extends Specification {
.verify(Duration.ofSeconds(4))
}
def "Closes for unknown transaction"() {
def "Wait for unknown transaction"() {
setup:
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain)
.setConfirmationLimit(6)
.setTxId(txId)
.build()
def exp1 = BlockchainOuterClass.TxStatus.newBuilder()
.setTxId(txId)
.setBroadcasted(false)
.setMined(false)
.build()
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
((EthereumChainUpstreams) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
_ * getFlux() >> Flux.empty()
}
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def scheduler = VirtualTimeScheduler.create(true)
trackTx.scheduler = scheduler
apiMock.answer("eth_getTransactionByHash", [txId], null)
when:
def act = StepVerifier.withVirtualTime {
return trackTx.subscribe(req)
}
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def act = StepVerifier.withVirtualTime(
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).castApi(EthereumApi.class)) },
{ scheduler },
5)
then:
act
.expectNext(exp1)
.then {
assert trackTx.notFound.any { it.tx.txid.toHex() == txId }
}
.expectNoEvent(Duration.ofSeconds(30))
.then {
def track = trackTx.notFound.iterator().next()
track.tx
.copy(Instant.now() - Duration.ofHours(1), track, track.tx.status, Instant.now() - Duration.ofMinutes(15))
.makeCurrent()
trackTx.recheckNotFound()
}
.expectNext(exp1)
.then {
assert !trackTx.notFound.any { it.tx.txid.toHex() == txId }
}
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(20)).as("Waited for updates")
.expectComplete()
.verify(Duration.ofSeconds(4))
.verify(Duration.ofSeconds(3))
}
def "Closes for known transaction if not mined"() {
def "Known transaction when not mined"() {
setup:
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain)
@@ -185,49 +173,70 @@ class TrackEthereumTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def scheduler = VirtualTimeScheduler.create(true)
trackTx.scheduler = scheduler
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
when:
def act = StepVerifier.withVirtualTime {
return trackTx.subscribe(req)
}
def act = StepVerifier.withVirtualTime({
return trackTx.subscribe(req).take(2)
}, { scheduler }, 5)
then:
act
.expectNext(exp1)
.then {
assert trackTx.notFound.any { it.tx.txid.toHex() == txId }
}
.expectNoEvent(Duration.ofSeconds(20))
.then {
def track = trackTx.notFound.iterator().next()
track.tx
.copy(Instant.now() - Duration.ofSeconds(119), track, track.tx.status, Instant.now() - Duration.ofMinutes(15))
.makeCurrent()
trackTx.recheckNotFound()
}
.expectNext(exp2)
.then {
assert trackTx.notFound.any { it.tx.txid.toHex() == txId }
}
.expectNoEvent(Duration.ofSeconds(20))
.then {
def track = trackTx.notFound.iterator().next()
track.tx
.copy(Instant.now() - Duration.ofSeconds(121), track, track.tx.status, Instant.now() - Duration.ofMinutes(15))
.makeCurrent()
trackTx.recheckNotFound()
}
.expectNext(exp2)
.expectSubscription()
.expectNext(exp1).as("Unknown tx")
.expectNext(exp2).as("Found in mempool")
.expectComplete()
.verify(Duration.ofSeconds(4))
}
def "New block makes tx mined"() {
setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer(
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
[TxId.from(txId)]
)
when:
def act = trackTx.onNewBlock(tx, block)
then:
StepVerifier.create(act)
.expectNext(tx.withStatus(true, 100, true, BlockHash.from(txId), block.timestamp, BigInteger.ONE, 1))
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "New block without current tx requires a call"() {
setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer(
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
)
apiMock.answer("eth_getTransactionByHash", [txId], null)
when:
def act = trackTx.onNewBlock(tx, block)
then:
StepVerifier.create(act)
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Starts to follow new transaction"() {
setup:
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
@@ -284,11 +293,11 @@ class TrackEthereumTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
apiMock.answerOnce("eth_getTransactionByHash", [txId], txJsonBroadcasted)
apiMock.answerOnce("eth_getTransactionByHash", [txId], txJsonBroadcasted)
apiMock.answer("eth_getTransactionByHash", [txId], txJsonMined)
blocks.forEach { block ->
apiMock.answer("eth_getBlockByHash", [block.hash.toHex(), false], block)
@@ -305,11 +314,11 @@ class TrackEthereumTxSpec extends Specification {
def flux = trackTx.subscribe(req)
then:
StepVerifier.create(flux)
.expectNext(exp1.build())
.expectNext(exp1.build()).as("Just empty")
.then(nextBlock(1))
.expectNext(exp1.setBroadcasted(true).build())
.expectNext(exp1.setBroadcasted(true).build()).as("Found in mempool")
.then(nextBlock(2))
.expectNext(exp2.setConfirmations(1).build())
.expectNext(exp2.setConfirmations(1).build()).as("Mined")
.then(nextBlock(3))
.expectNext(exp2.setConfirmations(2).build())
.then(nextBlock(4))
@@ -320,59 +329,4 @@ class TrackEthereumTxSpec extends Specification {
.verify(Duration.ofSeconds(4))
}
def "Tracked after first load"() {
setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain)
.setConfirmationLimit(6)
.setTxId(txId)
.build()
def tx = trackTx.prepareTracking(req)
when:
trackTx.onFirstUpdate(tx)
then:
trackTx.notFound.any { it.tx.txid == TransactionId.from(txId) }
trackTx.trackedForChain(Chain.ETHEREUM).any { it.txid == TransactionId.from(txId) }
}
def "Update of last notified keeps everything else"() {
setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain)
.setConfirmationLimit(6)
.setTxId(txId)
.build()
def tx = trackTx.prepareTracking(req)
when:
tx = tx.withStatus(true, 100,
true, BlockHash.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22"), Instant.now(),
1000 as BigInteger, 15L)
then:
tx.status.found
tx.notifiedAt.isBefore(Instant.now() - Duration.ofSeconds(5))
when:
def c = tx.justNotified()
then:
tx.notifiedAt.isBefore(Instant.now() - Duration.ofSeconds(5))
c.notifiedAt.isAfter(Instant.now() - Duration.ofSeconds(5))
c.status.found
c.status.height == 100
c.status.mined
c.status.blockHash.toHex() == "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22"
c.status.blockTime.isAfter(Instant.now() - Duration.ofSeconds(5))
c.status.blockTotalDifficulty == 1000 as BigInteger
c.status.confirmations == 15L
}
}