problem: Ethereum Address tracking has complex non-reactive logic
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user