problem: Ethereum Address tracking has complex non-reactive logic

This commit is contained in:
Igor Artamonov
2020-05-03 13:55:14 -04:00
parent 0f5c6c70a4
commit 3fcc2e6d52
3 changed files with 59 additions and 166 deletions

View File

@@ -26,7 +26,7 @@ import reactor.core.publisher.Mono
interface TrackAddress { interface TrackAddress {
fun isSupported(chain: Chain): Boolean fun isSupported(chain: Chain): Boolean
fun getBalance(requestMono: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
fun subscribe(requestMono: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
} }

View File

@@ -29,119 +29,50 @@ import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Commands import io.infinitape.etherjar.rpc.Commands
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.BlockTag import io.infinitape.etherjar.rpc.json.BlockTag
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import reactor.core.scheduler.Scheduler
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicLong
import javax.annotation.PostConstruct
@Service @Service
class TrackEthereumAddress( class TrackEthereumAddress(
@Autowired private val upstreams: Upstreams, @Autowired private val upstreams: Upstreams
@Autowired private val upstreamScheduler: Scheduler
) : TrackAddress { ) : TrackAddress {
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
private val seq = AtomicLong(0)
@PostConstruct
fun init() {
upstreams.observeChains().subscribe { chain ->
if (!clients.containsKey(chain)) {
clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { updateBalancesAll(chain) }
}
}
}
}
@Scheduled(fixedDelay = 120_000)
fun pingOld() {
val period = Duration.ofMinutes(15)
upstreams.getAvailable().forEach { chain ->
clients[chain]?.let { clients ->
clients.toFlux().filter {
it.lastPing < Instant.now().minus(period)
}.subscribe {
notify(it)
}
}
}
}
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
} }
private fun startTracking(client: TrackedAddress) { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
clients[client.chain]?.add(client) ?: log.warn("Chain ${client.chain} is not available for tracking") return initAddress(request)
} .flatMap { a -> getBalance(a).map { a.withBalance(it) } }
.map { buildResponse(it) }
private fun stopTracking(client: TrackedAddress) {
clients[client.chain]?.removeIf {
it.id == client.id
} ?: log.warn("Chain ${client.chain} is not available for tracking")
}
fun isTracked(chain: Chain, address: Address): Boolean {
return clients[chain]?.any { it.address == address } ?: false
}
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
val chain = Chain.byId(request.asset.chainValue)
if (!upstreams.isAvailable(chain)) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
if (request.asset.code?.toLowerCase() != "ether") {
return Flux.error(SilentException("Unsupported asset ${request.asset.code}"))
}
return when {
request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE ->
Flux.just(simpleAddress(request.address.addressSingle, chain))
request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI ->
Flux.fromIterable(request.address.addressMulti.addressesList)
.map { simpleAddress(it, chain) }
else -> {
log.error("Unsupported address type: ${request.address.addrTypeCase}")
Flux.empty()
}
}
}
private fun initializeSubscription(request: BlockchainOuterClass.BalanceRequest, observer: TopicProcessor<BlockchainOuterClass.AddressBalance>): Flux<TrackedAddress> {
return initializeSimple(request)
.map {
it.asTracked(observer, seq.incrementAndGet())
}
} }
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>() val chain = Chain.byId(request.asset.chainValue)
return initializeSubscription(request, bus) val head = upstreams.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
val balances = initAddress(request)
.flatMap { tracked -> .flatMap { tracked ->
val current = getBalance(tracked).map { val current = getBalance(tracked)
tracked.withBalance(it) .map {
}.doOnNext { tracked.withBalance(it)
startTracking(it) }
}.map { val updates = head
buildResponse(it) .flatMap {
} getBalance(tracked)
Flux.merge(current, bus).doFinally { stopTracking(tracked) } }.map {
tracked.withBalance(it)
}
Flux.concat(current, updates)
.distinctUntilChanged {
it.balance ?: Wei.ZERO
}
} }
.doOnError { t -> .doOnError { t ->
if (t is SilentException) { if (t is SilentException) {
@@ -153,34 +84,42 @@ class TrackEthereumAddress(
log.warn("Failed to process subscription", t) log.warn("Failed to process subscription", t)
} }
} }
return balances.map {
buildResponse(it)
}
} }
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
return initializeSimple(request) val chain = Chain.byId(request.asset.chainValue)
.flatMap { a -> getBalance(a).map { a.withBalance(it) } } if (!upstreams.isAvailable(chain)) {
.map { buildResponse(it) } return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
if (request.asset.code?.toLowerCase() != "ether") {
return Flux.error(SilentException("Unsupported asset ${request.asset.code}"))
}
return when (request.address.addrTypeCase) {
Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE ->
Flux.just(createAddress(request.address.addressSingle, chain))
Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI ->
Flux.fromIterable(request.address.addressMulti.addressesList)
.map { createAddress(it, chain) }
else -> {
log.error("Unsupported address type: ${request.address.addrTypeCase}")
Flux.empty()
}
}
} }
private fun simpleAddress(address: Common.SingleAddress, chain: Chain): SimpleAddress { private fun createAddress(address: Common.SingleAddress, chain: Chain): TrackedAddress {
val addressParsed = Address.from(address.address) val addressParsed = Address.from(address.address)
return SimpleAddress( return TrackedAddress(
chain, chain,
addressParsed addressParsed
) )
} }
private fun updateBalancesAll(chain: Chain) { fun getBalance(addr: TrackedAddress): Mono<Wei> {
clients[chain]?.let { all ->
all.toFlux()
.buffer(20)
.map { group ->
updateBalances(chain, group).subscribe { updated -> notify(updated) }
}
.subscribe()
}
}
fun getBalance(addr: SimpleAddress): Mono<Wei> {
val up = upstreams.getUpstream(addr.chain) as AggregatedUpstream<EthereumApi>? val up = upstreams.getUpstream(addr.chain) as AggregatedUpstream<EthereumApi>?
?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain)) ?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain))
return up.getApi(Selector.empty) return up.getApi(Selector.empty)
@@ -188,26 +127,7 @@ class TrackEthereumAddress(
.timeout(Defaults.timeout) .timeout(Defaults.timeout)
} }
private fun updateBalances(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> { private fun buildResponse(address: TrackedAddress): BlockchainOuterClass.AddressBalance {
val up = upstreams.getUpstream(chain) ?: return Flux.empty<TrackedAddress>()
return group.toFlux()
.parallel(8).runOn(upstreamScheduler)
.flatMap { a ->
getBalance(a).map { Update(a, it) }
}
.sequential()
.filter {
it.addr.balance == null || it.addr.balance != it.value
}
.doOnNext {
it.addr.balance = it.value
}
.map {
it.addr
}
}
private fun buildResponse(address: SimpleAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder() return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.amount!!.toString(10)) .setBalance(address.balance!!.amount!!.toString(10))
.setAsset(Common.Asset.newBuilder() .setAsset(Common.Asset.newBuilder()
@@ -217,28 +137,10 @@ class TrackEthereumAddress(
.build() .build()
} }
private fun notify(address: TrackedAddress) { class TrackedAddress(val chain: Chain,
address.lastPing = Instant.now() val address: Address,
address.stream.onNext(buildResponse(address)) val balance: Wei? = null
} ) {
fun withBalance(balance: Wei) = TrackedAddress(chain, address, balance)
class Update(val addr: TrackedAddress, val value: Wei)
open class SimpleAddress(val chain: Chain, val address: Address, var balance: Wei? = null) {
fun asTracked(stream: TopicProcessor<BlockchainOuterClass.AddressBalance>, id: Long): TrackedAddress {
return TrackedAddress(chain, stream, address, balance = this.balance, id = id)
}
open fun withBalance(balance: Wei) = SimpleAddress(chain, address, balance)
}
class TrackedAddress(chain: Chain,
val stream: TopicProcessor<BlockchainOuterClass.AddressBalance>,
address: Address,
var lastPing: Instant = Instant.now(),
balance: Wei? = null,
val id: Long
): SimpleAddress(chain, address, balance) {
override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance, id)
} }
} }

View File

@@ -63,8 +63,7 @@ class TrackEthereumAddressSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate()) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
trackAddress.init()
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
when: when:
@@ -74,7 +73,6 @@ class TrackEthereumAddressSpec extends Specification {
.expectNext(exp) .expectNext(exp)
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(3)) .verify(Duration.ofSeconds(3))
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
} }
def "recheck address after each block"() { def "recheck address after each block"() {
@@ -102,12 +100,10 @@ class TrackEthereumAddressSpec extends Specification {
return it return it
} }
def blocksBus = TopicProcessor.create()
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate()) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
trackAddress.init()
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98") apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98")
@@ -116,16 +112,11 @@ class TrackEthereumAddressSpec extends Specification {
then: then:
StepVerifier.create(flux) StepVerifier.create(flux)
.expectNext(exp1) .expectNext(exp1)
.then {
assert trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
}
.then { .then {
upstreamMock.nextBlock(BlockContainer.from(block2, TestingCommons.objectMapper())) upstreamMock.nextBlock(BlockContainer.from(block2, TestingCommons.objectMapper()))
} }
.expectNext(exp2) .expectNext(exp2)
.thenCancel() .thenCancel()
.verify(Duration.ofSeconds(3)) .verify(Duration.ofSeconds(3))
Thread.sleep(50)
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
} }
} }