support null topics and topic position in logs filter (#649)

This commit is contained in:
a10zn8
2025-04-04 17:09:08 +03:00
committed by GitHub
parent 6744e2241c
commit 9ca2c921b5
4 changed files with 55 additions and 20 deletions

View File

@@ -72,7 +72,7 @@ open class EthereumEgressSubscription(
data class LogsRequest( data class LogsRequest(
val address: List<Address>, val address: List<Address>,
val topics: List<Hex32>, val topics: List<Hex32?>,
) )
fun readLogsRequest(params: Map<String, Any?>): LogsRequest { fun readLogsRequest(params: Map<String, Any?>): LogsRequest {
@@ -98,7 +98,7 @@ open class EthereumEgressSubscription(
} else { } else {
emptyList() emptyList()
} }
val topics: List<Hex32> = if (params.containsKey("topics")) { val topics: List<Hex32?> = if (params.containsKey("topics")) {
when (val topics = params["topics"]) { when (val topics = params["topics"]) {
is String -> try { is String -> try {
listOf(Hex32.from(topics)) listOf(Hex32.from(topics))
@@ -106,15 +106,16 @@ open class EthereumEgressSubscription(
log.debug("Ignore invalid topic: $topics with error ${t.message}") log.debug("Ignore invalid topic: $topics with error ${t.message}")
emptyList() emptyList()
} }
is Collection<*> -> topics.mapNotNull { topic -> is Collection<*> -> topics.map { topic ->
try { try {
when (topic) { when (topic) {
null -> null
is Collection<*> -> topic.firstOrNull()?.toString()?.let { Hex32.from(it) } is Collection<*> -> topic.firstOrNull()?.toString()?.let { Hex32.from(it) }
else -> topic?.toString()?.let { Hex32.from(it) } else -> topic?.toString()?.let { Hex32.from(it) }
} }
} catch (t: Throwable) { } catch (t: Throwable) {
log.debug("Ignore invalid topic: $topic with error ${t.message}") log.debug("Ignore invalid topic: $topic with error ${t.message}")
null throw IllegalArgumentException("Invalid topic: $topic")
} }
} }
null -> emptyList() null -> emptyList()

View File

@@ -44,7 +44,7 @@ open class ConnectLogs(
return produceLogs.produce(connectBlockUpdates.connect(matcher)) return produceLogs.produce(connectBlockUpdates.connect(matcher))
} }
open fun create(addresses: List<Address>, topics: List<Hex32>): SubscriptionConnect<LogMessage> { open fun create(addresses: List<Address>, topics: List<Hex32?>): SubscriptionConnect<LogMessage> {
return object : SubscriptionConnect<LogMessage> { return object : SubscriptionConnect<LogMessage> {
override fun connect(matcher: Selector.Matcher): Flux<LogMessage> { override fun connect(matcher: Selector.Matcher): Flux<LogMessage> {
// shortcut to the whole output if we don't have any filters // shortcut to the whole output if we don't have any filters
@@ -58,22 +58,17 @@ open class ConnectLogs(
} }
} }
fun filtered(addresses: List<Address>, topics: List<Hex32>): Function<Flux<LogMessage>, Flux<LogMessage>> { fun filtered(addresses: List<Address>, selectedTopics: List<Hex32?>): Function<Flux<LogMessage>, Flux<LogMessage>> {
// sort search criteria to use binary search later // sort search criteria to use binary search later
val sortedAddresses: List<Address> = addresses.sortedWith(ADDR_COMPARATOR) val sortedAddresses: List<Address> = addresses.sortedWith(ADDR_COMPARATOR)
val sortedTopics: List<Hex32> = topics.sortedWith(TOPIC_COMPARATOR)
return Function { logs -> return Function { logs ->
logs.filter { logs.filter {
val goodAddress = val goodAddress =
sortedAddresses.isEmpty() || sortedAddresses.binarySearch(it.address, ADDR_COMPARATOR) >= 0 sortedAddresses.isEmpty() || sortedAddresses.binarySearch(it.address, ADDR_COMPARATOR) >= 0
val goodTopic = when { val goodTopic = when {
sortedTopics.isEmpty() -> true selectedTopics.isEmpty() -> true
it.topics.size < sortedTopics.size -> false it.topics.size < selectedTopics.size -> false
else -> sortedTopics.indices.all { index -> else -> selectedTopics.zip(it.topics).all { (selectedTopic, logTopic) -> selectedTopic == null || selectedTopic == logTopic }
it.topics[index].let { logTopic ->
sortedTopics.binarySearch(logTopic, TOPIC_COMPARATOR) >= 0
}
}
} }
goodAddress && goodTopic goodAddress && goodTopic
} }

View File

@@ -129,10 +129,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
]) ])
then: then:
act.address == [] thrown(IllegalArgumentException)
act.topics == [
Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
]
} }
def "read multi topic logs request"() { def "read multi topic logs request"() {
@@ -195,6 +192,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
] ]
act.topics == [ act.topics == [
Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
null,
Hex32.from("0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925") Hex32.from("0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925")
] ]
} }

View File

@@ -104,6 +104,23 @@ class ConnectLogsSpec extends Specification {
"upstream" "upstream"
) )
def log6 = new LogMessage(
Address.from("0x63bc4a36c66c64acb3d695298d492e8c1d909d3f"),
BlockHash.empty(),
100L,
HexData.empty(),
1L,
[
Hex32.from("0x952ba7f163c4a11628f55a4df523b3efddf252ad1be2c89b69c2b068fc378daa"),
Hex32.from("0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640"),
Hex32.from("0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5641")
],
TransactionId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec"),
1L,
false,
"upstream"
)
def "Filter is empty"() { def "Filter is empty"() {
setup: setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
@@ -174,7 +191,7 @@ class ConnectLogsSpec extends Specification {
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when: when:
def input = Flux.fromIterable([ def input = Flux.fromIterable([
log1, log2, log3, log4, log5 log1, log2, log3, log4, log5, log6
]) ])
def act = input.transform(connectLogs.filtered( def act = input.transform(connectLogs.filtered(
@@ -187,7 +204,31 @@ class ConnectLogsSpec extends Specification {
.collectList().block() .collectList().block()
then: then:
act.size() == 1 act.size() == 2
act[0] == log5 act[0] == log5
act[1] == log6
}
def "Filter by address and second topics"() {
setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when:
def input = Flux.fromIterable([
log1, log2, log3, log4, log5, log6
])
def act = input.transform(connectLogs.filtered(
[Address.from("0x63bc4a36c66c64acb3d695298d492e8c1d909d3f")],
[
Hex32.from("0x952ba7f163c4a11628f55a4df523b3efddf252ad1be2c89b69c2b068fc378daa"),
null,
Hex32.from("0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5641"),
]
))
.collectList().block()
then:
act.size() == 1
act[0] == log6
} }
} }