Skip to content

Conversation

@scarmuega
Copy link
Member

@scarmuega scarmuega commented Oct 6, 2025

Summary

This PR provides the initial scaffolding for adding multi-chain support to Oura. In particular, it introduces:

  • An updated pipeline model that supports multiple-chains
  • A stub of the Bitcoin source plugin
  • A stub of the Ethereum source plugin
  • A stub of the Substrate source plugin

In order to accomplish this, we had to refactor the following:

  • Cardano data is now namespaced under a Cardano record wrapper across filters, sinks, and sources.
  • The event and data model has a clear multi-chain handling mechanism using sum types.
  • All filters / sinks provide the previous behavior but with opting out of any non-Cardano chain (for the time being)

@coderabbitai
Copy link

coderabbitai bot commented Oct 6, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces multi-chain abstractions: new framework modules (cardano, ethereum, bitcoin, substrate), a Chain enum, and a chain-wrapped Record enum. Refactors Cardano paths across filters, sinks, and sources to use Record::Cardano and cardano::ChainConfig. Adds an optional Ethereum source (feature "eth") using alloy. Updates CLI args and daemon config to new chain types.

Changes

Cohort / File(s) Summary
Build: features & deps
Cargo.toml
Adds feature eth enabling futures-util. Adds dependency alloy = "1.0.30" with features = ["full"].
Framework restructuring (multi-chain)
src/framework/mod.rs, src/framework/cardano/mod.rs, src/framework/ethereum/mod.rs, src/framework/bitcoin/mod.rs, src/framework/substrate/mod.rs
Introduces Chain enum with Cardano(cardano::ChainConfig). Adds chain-specific Record enums and From<Record> for serde_json::Value. Re-exports Cardano types and adds Context. Removes top-level legacy_v1 module.
CLI binaries
src/bin/oura/dump.rs, src/bin/oura/watch.rs
Replaces prior Chain/ChainConfig usage with chain-scoped enums: DumpChain and WatchChain. Adds From<...> for cardano::ChainConfig. Constructs chains via Chain::Cardano(...). Adjusts args magic types.
Daemon config
src/daemon/mod.rs
Changes ConfigRoot.chain type from Option<ChainConfig> to Option<Chain>.
Filters: legacy v1 path updates
src/filters/legacy_v1/cip15.rs, .../cip25.rs, .../crawl.rs, .../map.rs, .../prelude.rs, src/sinks/assert/checks.rs, src/sinks/assert/prelude.rs
Updates imports from framework::legacy_v1::* to framework::cardano::legacy_v1::*. Adjusts emitted/appended events to be Cardano-wrapped where applicable.
Filters: record routing to Cardano
src/filters/parse_cbor.rs, src/filters/split_block.rs, src/filters/select/eval/mod.rs, src/filters/into_json.rs, src/filters/legacy_v1/mod.rs
Refactors matches and outputs to use Record::Cardano(cardano::Record::...). Select filter narrows to Cardano ParsedTx/ParsedBlock with explicit fallback. Legacy v1 bootstrap reads Cardano chain config from Chain.
Filters: WASM plugin
src/filters/wasm_plugin.rs
Adds type CardanoRecord = cardano::Record. Dispatches Record::Cardano to map_cardano_record; passes through non-Cardano records unchanged.
Sinks: terminal & aws
src/sinks/terminal/format.rs, src/sinks/aws_s3.rs
Terminal formatter now matches Record::Cardano(cardano::Record::...) and updates legacy v1 paths. AWS S3 sink matches Cardano-wrapped CBOR blocks.
Sinks: assert
src/sinks/assert/mod.rs
Matches legacy events via Record::Cardano(cardano::Record::OuraV1Event(...)).
Sources: Cardano wrapping & config extraction
src/sources/hydra.rs, src/sources/mithril.rs, src/sources/n2c.rs, src/sources/n2n.rs, src/sources/s3.rs, src/sources/u5c.rs
Emits ChainEvent::Apply with Record::Cardano(cardano::Record::...). During bootstrap where needed, extracts cardano::ChainConfig from Chain::Cardano(...).
Sources: orchestrator + feature gate
src/sources/mod.rs
Adds #[cfg(feature = "eth")] module eth. Extends Bootstrapper, Config, and related methods with Ethereum variants behind feature flag.
New Ethereum source
src/sources/eth.rs
Adds WebSocket-based Ethereum source using alloy. Defines Config, Stage, Worker. Subscribes to block headers, fetches blocks, emits ChainEvent::Apply with Record::Ethereum(ParsedBlock).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Pipeline
  participant Sources as Sources::Config
  participant Eth as sources::eth::Worker
  participant WS as Ethereum WS Provider (alloy)
  participant StageOut as Stage Output

  User->>Pipeline: Run with feature "eth" and URL
  Pipeline->>Sources: Config::bootstrapper(Ethereum{ url })
  Sources-->>Pipeline: Bootstrapper::Ethereum(Stage)
  Note right of Pipeline: spawn()
  Pipeline->>Eth: start(Stage, Worker)

  rect rgba(230,240,255,0.5)
    Eth->>WS: connect_ws(url)
    Eth->>WS: subscribe_blocks()
    WS-->>Eth: stream of block headers
  end

  loop schedule
    Eth->>Eth: poll next header
    alt header available
      Eth-->>Pipeline: Work::Unit{ hash }
      Pipeline->>Eth: Execute(Unit{ hash })
      Eth->>WS: get_block_by_hash(hash)
      WS-->>Eth: Block or None
      alt Block found
        Eth->>StageOut: ChainEvent::Apply(point, Record::Ethereum(ParsedBlock))
      else No block
        Eth->>StageOut: log warn / no event
      end
    else idle
      Eth-->>Pipeline: Work::Idle
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws on multi-chain ground,
New lanes for bytes now hop around.
Cardano wrapped, Ethereum streams,
Blocks like carrots, crunchy dreams.
Feature flags flick, the burrow hums—
Alloy winds, a header comes.
Onward I nibble through JSON crumbs. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “refactor: scaffold multi-chain pipeline” succinctly and accurately captures the primary change of restructuring the codebase to support multiple blockchain pipelines without including extraneous detail. It clearly follows a conventional commit style, highlights the refactor scope, and indicates the main feature addition. This makes it easy for a reviewer or teammate to understand the essence of the pull request at a glance.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@scarmuega scarmuega marked this pull request as ready for review October 6, 2025 22:55
@scarmuega
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 6, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/sources/eth.rs (1)

56-57: Track multi-chain Point support.

The TODO comment indicates that multi-chain Point support is needed. Currently, pallas::network::miniprotocols::Point::Origin is used as a placeholder.

Do you want me to open a new issue to track implementing proper multi-chain Point support for Ethereum?

src/framework/substrate/mod.rs (1)

1-15: Use a Substrate-specific block representation

alloy::rpc::types::Block is an Ethereum block; wiring it into the Substrate record means we can’t ever emit an actual Substrate block without serializing it into Ethereum’s shape. Please swap this for a Substrate-native type (e.g. from subxt/sp_runtime) or keep the placeholder as a neutral struct until that integration lands.

src/sinks/terminal/format.rs (1)

100-101: Silence unused bindings on todo!() arms.

Both the Bitcoin and Substrate matches bind record but never use it, which triggers compiler warnings. Rename them to _record (or just _) so the build stays warning-free.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1af8f27 and 43aad01.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • Cargo.toml (2 hunks)
  • src/bin/oura/dump.rs (4 hunks)
  • src/bin/oura/watch.rs (4 hunks)
  • src/daemon/mod.rs (1 hunks)
  • src/filters/into_json.rs (1 hunks)
  • src/filters/legacy_v1/cip15.rs (1 hunks)
  • src/filters/legacy_v1/cip25.rs (1 hunks)
  • src/filters/legacy_v1/crawl.rs (1 hunks)
  • src/filters/legacy_v1/map.rs (1 hunks)
  • src/filters/legacy_v1/mod.rs (2 hunks)
  • src/filters/legacy_v1/prelude.rs (2 hunks)
  • src/filters/parse_cbor.rs (1 hunks)
  • src/filters/select/eval/mod.rs (1 hunks)
  • src/filters/split_block.rs (1 hunks)
  • src/filters/wasm_plugin.rs (3 hunks)
  • src/framework/bitcoin/mod.rs (1 hunks)
  • src/framework/cardano/mod.rs (1 hunks)
  • src/framework/ethereum/mod.rs (1 hunks)
  • src/framework/mod.rs (2 hunks)
  • src/framework/substrate/mod.rs (1 hunks)
  • src/sinks/assert/checks.rs (1 hunks)
  • src/sinks/assert/mod.rs (2 hunks)
  • src/sinks/assert/prelude.rs (1 hunks)
  • src/sinks/aws_s3.rs (1 hunks)
  • src/sinks/terminal/format.rs (4 hunks)
  • src/sources/eth.rs (1 hunks)
  • src/sources/hydra.rs (2 hunks)
  • src/sources/mithril.rs (1 hunks)
  • src/sources/mod.rs (6 hunks)
  • src/sources/n2c.rs (2 hunks)
  • src/sources/n2n.rs (2 hunks)
  • src/sources/s3.rs (1 hunks)
  • src/sources/u5c.rs (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Check (ubuntu-latest, stable)
src/sinks/terminal/format.rs

[warning] 101-101:
unused variable: record


[warning] 100-100:
unused variable: record

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Suite
  • GitHub Check: Check (windows-latest, stable)
🔇 Additional comments (18)
src/sinks/assert/prelude.rs (1)

1-1: LGTM! Import path correctly updated for multi-chain structure.

The import path change aligns with the broader refactoring to organize Cardano-specific types under the cardano namespace.

src/sinks/aws_s3.rs (1)

43-47: LGTM! Cardano record wrapping correctly implemented.

The pattern matching correctly wraps the CBOR block in the Cardano-specific record variant, consistent with the multi-chain refactoring across the codebase.

src/sources/u5c.rs (1)

34-38: LGTM! Cardano record wrapping correctly applied to both code paths.

Both the ParsedBlock and CborBlock variants are correctly wrapped in the Cardano-specific record type, maintaining consistency with the multi-chain architecture.

src/filters/legacy_v1/crawl.rs (1)

6-6: LGTM! Import path correctly updated.

The import path change from framework::legacy_v1 to framework::cardano::legacy_v1 aligns with the namespace reorganization for multi-chain support.

src/sources/s3.rs (1)

112-115: LGTM! Cardano record wrapping correctly implemented.

The ChainEvent::Apply construction correctly wraps the CBOR block in the Cardano-specific record variant, consistent with the multi-chain refactoring.

src/sources/mithril.rs (1)

318-319: LGTM! Cardano record wrapping correctly implemented.

The ChainEvent::Apply construction correctly wraps the CBOR block in the Cardano-specific record variant, maintaining consistency with the multi-chain architecture.

src/filters/into_json.rs (1)

29-32: LGTM! Multi-chain JSON conversion correctly implemented.

The pattern matching correctly converts Cardano records to JSON while preserving other record types unchanged. This enables multi-chain support in the JSON filter.

src/sinks/assert/mod.rs (2)

5-5: LGTM!

The import path correctly reflects the new Cardano-specific module structure for legacy_v1 events.


93-93: LGTM!

The pattern match correctly handles the new Cardano-wrapped record structure, maintaining the same logic while adapting to the multi-chain abstraction.

src/filters/legacy_v1/prelude.rs (2)

1-1: LGTM!

The import path correctly uses the Cardano-specific legacy_v1 module, aligning with the multi-chain framework structure.


45-48: LGTM!

The event construction correctly wraps the OuraV1Event in the Cardano-specific record wrapper, maintaining consistency with the multi-chain abstraction.

src/filters/legacy_v1/mod.rs (1)

42-42: LGTM!

The pattern match correctly handles the Cardano-wrapped CborBlock variant, consistent with the multi-chain refactoring.

src/sources/n2n.rs (1)

122-122: LGTM!

The record construction correctly wraps the CborBlock in the Cardano-specific variant, maintaining consistency with the multi-chain framework.

src/sources/n2c.rs (1)

106-107: LGTM!

The ChainEvent construction properly wraps the CborBlock in the Cardano-specific record variant, consistent with the multi-chain refactoring.

src/filters/parse_cbor.rs (2)

43-46: LGTM!

The pattern matching and output construction correctly handle Cardano-wrapped CborBlock records, transforming them into ParsedBlock records while maintaining the Cardano wrapper.


48-51: LGTM!

The CborTx handling follows the same pattern as CborBlock, correctly wrapping both input and output in the Cardano-specific record variant.

src/daemon/mod.rs (1)

19-19: Chain::default aligns with expected behavior
Default returns Chain::Cardano(Default::default()), matching the sole variant’s default config.

src/framework/ethereum/mod.rs (1)

1-17: Alloy Block JSON serialization verified: The alloy::rpc::types::Block struct in v1.0.30 derives Serialize with the correct serde attributes (e.g., rename_all="camelCase", flatten), so json!(x) will include all expected fields.

Comment on lines +87 to +88
# TODO(p): add feature
alloy = { version = "1.0.30", features = ["full"] }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make alloy dependency optional and feature-gated.

The alloy dependency is currently unconditional despite the TODO comment suggesting it should be feature-gated. This adds unnecessary dependencies and increases build times for users not using Ethereum features.

Apply this diff to make alloy optional and gated by the eth feature:

-# TODO(p): add feature
-alloy = { version = "1.0.30", features = ["full"] }
+alloy = { version = "1.0.30", features = ["full"], optional = true }

Then update the eth feature definition at line 23 to include alloy:

-eth = ["futures-util"]
+eth = ["futures-util", "alloy"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# TODO(p): add feature
alloy = { version = "1.0.30", features = ["full"] }
# In the [features] section (around line 23):
eth = ["futures-util", "alloy"]
# In the [dependencies] section (around lines 87–88):
alloy = { version = "1.0.30", features = ["full"], optional = true }
🤖 Prompt for AI Agents
In Cargo.toml around lines 87-88, the alloy dependency is unconditional; make it
optional and feature-gated by adding optional = true to the alloy dependency
entry and keeping its features = ["full"], then update the [features] section at
line 23 to include alloy by adding "alloy" to the eth feature list (i.e., make
eth = ["existing-items...", "alloy"]), so the crate is only pulled in when the
eth feature is enabled.

Comment on lines +92 to +94
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non-exhaustive pattern matching will panic on non-Cardano chains.

The pattern match on ctx.chain only handles the Chain::Cardano variant. According to the PR summary, the Chain enum includes Ethereum, Bitcoin, and Substrate variants. This will panic at runtime if a non-Cardano chain is configured.

Add proper error handling for non-Cardano chains:

-        let chain_config = match &ctx.chain {
-            Chain::Cardano(chain_config) => chain_config.clone(),
-        };
+        let chain_config = match &ctx.chain {
+            Chain::Cardano(chain_config) => chain_config.clone(),
+            _ => return Err(Error::config("legacy_v1 filter only supports Cardano chain")),
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
};
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
_ => return Err(Error::config("legacy_v1 filter only supports Cardano chain")),
};
🤖 Prompt for AI Agents
In src/filters/legacy_v1/mod.rs around lines 92-94, the match on ctx.chain only
handles Chain::Cardano and will panic for other variants; change it to an
exhaustive match that either extracts the Cardano config or returns a proper
error for non-Cardano variants (include the actual variant in the message), e.g.
match on Chain::{Cardano => chain_config.clone(), other => return Err(/*
appropriate error type with descriptive message including other */)}; ensure the
function signature and error type are adjusted/propagated accordingly instead of
letting the code panic.


let subscription = provider.subscribe_blocks().await.or_panic()?;

let stream = subscription.into_stream().take(2);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Remove the .take(2) limit on the block stream.

The stream is artificially limited to only 2 blocks, which will cause the source to stop processing after 2 blocks. This appears to be leftover test or debug code.

Apply this diff to remove the limit:

-        let stream = subscription.into_stream().take(2);
+        let stream = subscription.into_stream();
🤖 Prompt for AI Agents
In src/sources/eth.rs around line 37, the block stream is incorrectly limited by
.take(2) which causes processing to stop after two blocks; remove the .take(2)
call so the code uses subscription.into_stream() (i.e., let stream =
subscription.into_stream();) ensuring the stream is unbounded and continues to
yield blocks; update any downstream code/comments if they assumed a bounded
stream.

}

#[derive(Stage)]
#[stage(name = "source-utxorpc", unit = "Header", worker = "Worker")]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect stage name.

The stage name is set to "source-utxorpc", but this is an Ethereum source, not a UTXORPC source. This appears to be a copy-paste error.

Apply this diff to correct the stage name:

-#[stage(name = "source-utxorpc", unit = "Header", worker = "Worker")]
+#[stage(name = "source-eth", unit = "Header", worker = "Worker")]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[stage(name = "source-utxorpc", unit = "Header", worker = "Worker")]
#[stage(name = "source-eth", unit = "Header", worker = "Worker")]
🤖 Prompt for AI Agents
In src/sources/eth.rs around line 68, the stage attribute incorrectly uses name
= "source-utxorpc"; update the stage attribute to the correct identifier for
this Ethereum source (e.g., name = "source-eth" or the project's canonical
ethereum source name) so it reflects that this is an Ethereum source rather than
UTXORPC; keep the rest of the attribute (unit = "Header", worker = "Worker")
unchanged.

Comment on lines +204 to +206
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non-exhaustive pattern matching will panic on non-Cardano chains.

This pattern match only handles Chain::Cardano, which will cause a panic if a non-Cardano chain is configured.

Add proper error handling:

-        let chain_config = match &ctx.chain {
-            Chain::Cardano(chain_config) => chain_config.clone(),
-        };
+        let chain_config = match &ctx.chain {
+            Chain::Cardano(chain_config) => chain_config.clone(),
+            _ => return Err(Error::config("n2c source only supports Cardano chain")),
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
};
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
_ => return Err(Error::config("n2c source only supports Cardano chain")),
};

Comment on lines +228 to +230
let chain_config = match &ctx.chain {
Chain::Cardano(chain_config) => chain_config.clone(),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non-exhaustive pattern matching will panic on non-Cardano chains.

Similar to the legacy_v1 filter, this pattern match only handles Chain::Cardano. This will panic if a non-Cardano chain is configured.

Add proper error handling:

-        let chain_config = match &ctx.chain {
-            Chain::Cardano(chain_config) => chain_config.clone(),
-        };
+        let chain_config = match &ctx.chain {
+            Chain::Cardano(chain_config) => chain_config.clone(),
+            _ => return Err(Error::config("n2n source only supports Cardano chain")),
+        };
🤖 Prompt for AI Agents
In src/sources/n2n.rs around lines 228 to 230, the match on ctx.chain only
handles Chain::Cardano and will panic for other variants; change it to handle
all Chain variants explicitly (or use an if let with an else) and return or
propagate a proper error instead of unwrapping/panicking. Specifically, detect
non-Cardano chains and return a Result::Err (or appropriate error type) with a
clear message, or add matching arms for other Chain variants that provide
correct behavior, ensuring no panics from non-exhaustive pattern matching.

@scarmuega scarmuega merged commit a7359e3 into v3 Oct 7, 2025
10 of 11 checks passed
This was referenced Oct 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants