Skip to content

EVM-17: EvmTransactionProcessor domain service #157

Description

@Puneethkumarck

Context

The EvmTransactionProcessor is the core domain service that takes a batch of confirmed EVM transactions and an associated block, classifies each transaction, and writes the results to the appropriate repositories in parallel using virtual threads. It mirrors the existing Solana TransactionProcessor pattern exactly: split transactions into categories (successful, failed, large transfers, token transfers), write each category to its repository concurrently, save the block, and record batch metrics.

Specification

File

prism/src/main/java/com/stablebridge/prism/domain/service/EvmTransactionProcessor.java

Constructor dependencies (injected via Avaje)

  • EvmTransactionRepository — for successful transactions (COPY protocol)
  • EvmFailedTransactionRepository — for failed/reverted transactions
  • EvmTransferRepository — for large native transfers
  • EvmTokenTransferRepository — for ERC-20 token transfers
  • EvmBlockRepository — for block metadata
  • MetricsRecorder — for recording batch processing metrics
  • IndexerConfig — for large transfer threshold

Processing logic

public EvmBatchResult processBatch(List<EvmTransaction> transactions, EvmBlock block) {
    // 1. Split transactions
    var successful = transactions.stream().filter(EvmTransaction::status).toList();
    var failed = transactions.stream().filter(tx -> !tx.status()).toList();

    // 2. Extract large native transfers (successful only, exclude OP deposit type 126)
    var threshold = config.largeTransferThreshold();
    var largeTransfers = successful.stream()
        .filter(tx -> tx.type() != 126)  // exclude OP Stack deposits
        .filter(tx -> EvmLargeTransferFilter.isLargeTransfer(tx.value(), threshold))
        .map(EvmTransaction::toLargeTransfer)
        .toList();

    // 3. Extract ERC-20 token transfers from all transaction logs
    var tokenTransfers = transactions.stream()
        .flatMap(tx -> tx.extractTokenTransfers().stream())
        .toList();

    // 4. Parallel writes via virtual threads
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var txFuture = executor.submit(() -> transactionRepo.saveAll(successful));
        var failedFuture = executor.submit(() -> failedTransactionRepo.saveAll(
            failed.stream().map(EvmTransaction::toFailedTransaction).toList()));
        var transferFuture = executor.submit(() -> transferRepo.saveAll(largeTransfers));
        var tokenFuture = executor.submit(() -> tokenTransferRepo.saveAll(tokenTransfers));
        var blockFuture = executor.submit(() -> blockRepo.save(block));

        // Wait for all writes to complete
        txFuture.get();
        failedFuture.get();
        transferFuture.get();
        tokenFuture.get();
        blockFuture.get();
    }

    // 5. Record metrics
    var result = new EvmBatchResult(
        successful.size(), failed.size(),
        largeTransfers.size(), tokenTransfers.size(), 0);
    metricsRecorder.recordBatch(result);
    return result;
}

Design decisions

  • Follow existing TransactionProcessor.java pattern exactly
  • 5 parallel virtual thread writes via Executors.newVirtualThreadPerTaskExecutor() in try-with-resources
  • OP Stack deposit transactions (type 126) are excluded from large transfer detection — these are L1-to-L2 bridge deposits that appear as native value transfers but are system operations
  • Token transfers are extracted from ALL transactions (not just successful) because logs in failed transactions can still be relevant for indexing
  • MetricsRecorder integration for observability
  • @Singleton annotation (Avaje DI), @RequiredArgsConstructor (Lombok)

Test class

prism/src/test/java/com/stablebridge/prism/domain/service/EvmTransactionProcessorTest.java

Test cases:

  • Batch with all successful transactions writes to transaction repo only
  • Batch with failed transactions writes to failed transaction repo
  • Large native transfer above threshold is written to transfer repo
  • OP Stack deposit (type 126) above threshold is NOT written to transfer repo
  • ERC-20 token transfers are extracted and written to token transfer repo
  • Block is saved to block repo
  • All 5 writes happen in parallel (verify via mock interactions)
  • Metrics are recorded after processing
  • Empty batch produces zero-count result
  • Exception in one write propagates correctly

Acceptance Criteria

  • EvmTransactionProcessor exists with correct constructor dependencies
  • processBatch() splits transactions into successful, failed, large transfers, token transfers
  • OP Stack deposit (type 126) transactions are excluded from large transfer detection
  • 5 parallel writes use Executors.newVirtualThreadPerTaskExecutor()
  • Block is saved to EvmBlockRepository
  • Metrics are recorded via MetricsRecorder
  • Returns accurate EvmBatchResult
  • All test cases pass
  • ./gradlew build passes

Dependencies

References

Metadata

Metadata

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions