Files
dshackle/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumEgressSubscription.kt
2026-05-26 17:13:31 +04:00

204 lines
8.4 KiB
Kotlin

package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
import io.emeraldpay.dshackle.upstream.ethereum.hex.Hex32
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectLogs
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectNewHeads
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
data class Transaction(
val blockHash: String?,
val blockNumber: String?,
val from: String,
val gas: String,
val gasPrice: String?,
val maxFeePerGas: String?,
val maxPriorityFeePerGas: String?,
val hash: String,
val input: String,
val nonce: String,
val to: String,
val transactionIndex: String?,
val value: String,
val type: String,
val accessList: List<Any>?,
val chainId: String,
val v: String,
val yParity: String?,
val r: String?,
val s: String?,
)
open class EthereumEgressSubscription(
val upstream: Multistream,
val scheduler: Scheduler,
val pendingTxesSource: PendingTxesSource?,
) : EgressSubscription {
companion object {
private val log = LoggerFactory.getLogger(EthereumEgressSubscription::class.java)
const val METHOD_NEW_HEADS = "newHeads"
const val METHOD_LOGS = "logs"
const val METHOD_PENDING_TXES = "newPendingTransactions"
const val METHOD_DRPC_PENDING_TXES = "drpc_pendingTransactions"
}
private val newHeads = ConnectNewHeads(upstream, scheduler)
open val logs = ConnectLogs(upstream, scheduler)
override fun getAvailableTopics(): List<String> {
val subs = if (upstream.getCapabilities().contains(Capability.WS_HEAD)) {
// we can't use logs without eth_getLogs method available
if (upstream.getMethods().isAvailable("eth_getLogs")) {
listOf(METHOD_NEW_HEADS, METHOD_LOGS)
} else {
listOf(METHOD_NEW_HEADS)
}
} else {
listOf()
}
return if (pendingTxesSource != null && pendingTxesSource !is NoPendingTxes && upstream.getCapabilities().contains(Capability.WS_PENDING_TX)) {
subs.plus(listOf(METHOD_PENDING_TXES, METHOD_DRPC_PENDING_TXES))
} else {
subs
}
}
@Suppress("UNCHECKED_CAST")
override fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher, unsubscribeMethod: String): Flux<out Any> {
if (topic == METHOD_NEW_HEADS) {
return newHeads.connect(matcher)
}
if (topic == METHOD_LOGS) {
val paramsMap = try {
if (params != null && Map::class.java.isAssignableFrom(params.javaClass)) {
readLogsRequest(params as Map<String, Any?>)
} else {
LogsRequest(emptyList(), emptyList())
}
} catch (t: Throwable) {
return Flux.error(UnsupportedOperationException("Invalid parameter for $topic. Error: ${t.message}"))
}
return logs.create(paramsMap.address, paramsMap.topics).connect(matcher)
}
if (topic == METHOD_PENDING_TXES) {
return pendingTxesSource?.connect(matcher) ?: Flux.empty()
} else if (topic == METHOD_DRPC_PENDING_TXES) {
return pendingTxesSource?.connect(matcher)?.flatMap { txHash ->
// Create request to get full transaction
val request = ChainRequest(
"eth_getTransactionByHash",
ListParams(txHash.toString()),
)
// try to read froms each upstream
Flux.fromIterable(upstream.getUpstreams())
.flatMap { currentUpstream ->
currentUpstream.getIngressReader().read(request)
.timeout(Defaults.internalCallsTimeout)
.flatMap { response ->
val result = response.getResult()
if (result.isEmpty()) {
Mono.empty()
} else {
Mono.just(Global.objectMapper.readValue(result, Transaction::class.java))
}
}
.doOnError { err ->
log.debug("Failed to get response from upstream ${currentUpstream.getId()} tx: $txHash: ${err.message}")
}
.onErrorResume { Mono.empty() }
}
.next()
.doOnSuccess { resp -> log.debug("eth_getTransactionByHash got response: $resp") }
.doOnError { err -> log.warn("eth_getTransactionByHash failed to get response from any upstream tx: $txHash: ${err.message}") }
} ?: Flux.empty()
}
return Flux.error(UnsupportedOperationException("Method $topic is not supported"))
}
data class LogsRequest(
val address: List<Address>,
val topics: List<List<Hex32>?>,
)
fun readLogsRequest(params: Map<String, Any?>): LogsRequest {
val addresses: List<Address> = if (params.containsKey("address")) {
when (val address = params["address"]) {
is String -> try {
listOf(Address.from(address))
} catch (t: Throwable) {
log.debug("Ignore invalid address: $address with error ${t.message}")
emptyList()
}
is Collection<*> -> address.mapNotNull {
try {
Address.from(it.toString())
} catch (t: Throwable) {
log.debug("Ignore invalid address: $address with error ${t.message}")
null
}
}
null -> emptyList()
else -> throw IllegalArgumentException("Invalid type of address field. Must be string or list of strings")
}
} else {
emptyList()
}
val topics: List<List<Hex32>?> = if (params.containsKey("topics")) {
when (val rawTopics = params["topics"]) {
is String -> try {
listOf(listOf(Hex32.from(rawTopics)))
} catch (t: Throwable) {
log.debug("Ignore invalid topic: $rawTopics with error ${t.message}")
emptyList()
}
is Collection<*> -> rawTopics.map { topicItem ->
when (topicItem) {
null -> null
is String -> listOfNotNull(
try {
Hex32.from(topicItem)
} catch (t: Throwable) {
log.debug("Ignore invalid topic: $topicItem with error ${t.message}")
null
},
)
is Collection<*> -> topicItem.mapNotNull { t ->
try {
t?.toString()?.let { Hex32.from(it) }
} catch (t: Throwable) {
log.debug("Ignore invalid topic: $t with error ${t.message}")
null
}
}
else -> throw IllegalArgumentException("Invalid topic entry: $topicItem. Must be null, string or list of strings")
}
}
null -> emptyList()
else -> throw IllegalArgumentException("Invalid type of topics field. Must be string or list of strings")
}
} else {
emptyList()
}
return LogsRequest(addresses, topics)
}
}