Skip to content

feat: oracle signed context - full stack (Phases 3-7)#2503

Closed
hardyjosh wants to merge 25 commits intomainfrom
feat/extract-oracle-url-sdk
Closed

feat: oracle signed context - full stack (Phases 3-7)#2503
hardyjosh wants to merge 25 commits intomainfrom
feat/extract-oracle-url-sdk

Conversation

@hardyjosh
Copy link
Contributor

@hardyjosh hardyjosh commented Mar 11, 2026

Merges the full oracle signed context stack into main. This includes:

These were stacked PRs that each merged into the previous branch but the chain was never merged to main. Only Phase 2 (#2457 - type definitions) landed in main.

Without this, oracle-url in strategy files is silently ignored during deployment, and the frontend can't display oracle info or fetch signed context for quotes.

Summary by CodeRabbit

Release Notes

  • New Features
    • Orders now support oracle URL configuration
    • Oracle URLs are displayed in order details
    • System automatically fetches and integrates oracle signed context when available

Josh Hardy added 25 commits February 13, 2026 18:22
…n RaindexOrder

- Add SignedContextOracleV1 variant to ParsedMeta enum
- Add match arm for KnownMagic::SignedContextOracleV1 in parsing logic
- Add oracle_url() wasm_bindgen getter on RaindexOrder
- Depends on rain.metadata feat/signed-context-oracle-meta branch
- Add 3 tests for SignedContextOracleV1 parsing (from_meta_item,
  parse_multiple, parse_from_bytes roundtrip)
- Add oracle_url() to non-wasm impl block (mirrors wasm getter)
- All 11 parsed_meta tests passing
…leV1 metadata

Points rain.interpreter at feat/signed-context-oracle-meta-submodule branch
which updates the rain.metadata submodule to feat/signed-context-oracle-meta.
Matches rename in rain.metadata#92. The metadata type is specific to
the Raindex calculateOrderIO entrypoint.
… flows

Phase 3 of signed context oracle discovery:

- New oracle.rs module with fetch_signed_context(url) and OracleResponse type
- OracleResponse maps directly to SignedContextV1 (signer, context as bytes32[], signature)
- Added signed_context field to TakeOrderCandidate
- Wired oracle fetching into:
  - build_take_order_candidates_for_pair (batch flow, concurrent fetch)
  - execute_single_take (single take flow, oracle_url param)
  - build_take_orders_config_from_simulation (passes through to TakeOrderConfigV4)
- Oracle fetch is best-effort: failures log a warning and use empty signed context
- 3 oracle tests + 9 parsed_meta tests passing
- Add get_order_quotes_with_context() to quote crate (accepts signed_context param)
- RaindexOrder.get_quotes() now fetches oracle data and passes to quotes
- Original get_order_quotes() unchanged (delegates with empty context)
reqwest::ClientBuilder::timeout() is not available on WASM targets.
Use cfg(not(target_family = "wasm")) to only set it on native.
- OrderDetail: show Oracle card property with URL link when order has oracle metadata
  - Includes tooltip explaining signed context oracle usage
- TanstackOrderQuote: show purple 'Oracle' badge next to quotes heading when oracle is active
  - Indicates quotes include signed context data from oracle
- Both use the oracleUrl getter exposed via WASM bindings on RaindexOrder
The oracle endpoint now receives order details via POST so it can tailor
responses based on the specific order, counterparty, and IO indexes.

POST body: abi.encode(OrderV4, inputIOIndex, outputIOIndex, counterparty)

Falls back to GET when no body is provided (simple oracles).
Callers currently pass None - ABI encoding will be wired in once the
order data is available at each call site.
POST with ABI-encoded order data is mandatory. Callers currently pass
empty vec — will be wired to abi.encode(OrderV4, inputIOIndex,
outputIOIndex, counterparty) at each call site.
- encode_oracle_body: abi.encode(OrderV4, inputIOIndex, outputIOIndex, counterparty)
- get_quotes: fetches oracle per IO pair concurrently, counterparty=address(0)
- build_take_order_candidates: fetches oracle per quote pair
- execute_single_take: encodes with actual taker as counterparty
- get_order_quotes_with_context_fn: accepts per-pair context callback
- Oracle fetch logic moved from common to quote crate (common re-exports)
- get_order_quotes now extracts oracle URL directly from SgOrder.meta
- Removed get_order_quotes_with_context and get_order_quotes_with_context_fn
- No more closures, HashMaps, or pre-fetching — oracle context fetched
  inline per IO pair inside the quote loop
- RaindexOrder.get_quotes() simplified to just call get_order_quotes()
- Add encode_oracle_body_batch() for array encoding: abi.encode((OrderV4, uint256, uint256, address)[])
- Update fetch_signed_context_batch() to handle Vec responses
- Maintain backward compatibility with single request functions
- Add comprehensive tests for both single and batch formats
- Response format now expects JSON array per spec
Adds optional oracle-url field to OrderCfg, parsed from the YAML front
matter orders section. When present, this URL identifies a signed context
oracle server for the order.

Changes:
- Add oracle_url: Option<String> to OrderCfg struct
- Parse oracle-url via optional_string in YAML parsing
- Add oracle-url to ALLOWED_ORDER_KEYS
- Update Default and PartialEq impls
- Add test for oracle-url parsing (present + absent)

Spec: rainlanguage/specs#45
Chained on: #2459 (Phase 4)
When an order has oracle_url set in its config, new_from_deployment now
creates a SignedContextOracleV1 meta item and includes it in the order's
additional_meta. This means orders deployed with oracle-url in their YAML
will have the oracle endpoint encoded in their onchain RainMetaDocumentV1,
making it discoverable by takers and indexers (Phase 2 reads it back).

Changes:
- Import SignedContextOracleV1 in add_order.rs
- In new_from_deployment: parse oracle_url, create meta item, append to
  additional_meta
- Extracts oracle URL from raw meta bytes
- CBOR decodes and finds RaindexSignedContextOracleV1 by magic number
- Available in both Rust and WASM/JS APIs
- Returns Option<String> (URL if found, None otherwise)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

Walkthrough

This pull request adds comprehensive oracle support to the Rain Orderbook system. It introduces a new oracle module for fetching signed context from oracle endpoints, extends metadata parsing to support oracle configuration, adds oracle_url fields to order settings, plumbs oracle URLs through quote generation and order processing pipelines, and updates the UI to display oracle endpoints.

Changes

Cohort / File(s) Summary
Oracle Module
crates/quote/Cargo.toml, crates/quote/src/lib.rs, crates/quote/src/oracle.rs
New oracle module with request/response handling, encoding functions for single and batch oracle requests, async fetchers for signed context, error types, and unit tests; dependencies on rain-metadata and futures added.
Metadata & Parsing Support
crates/common/src/add_order.rs, crates/common/src/oracle.rs, crates/common/src/lib.rs, crates/common/src/parsed_meta.rs
Adds RaindexSignedContextOracleV1 to metadata imports and parsing; creates oracle module re-export in common crate; extends ParsedMeta enum with oracle variant and from_meta_item handling; test scaffolding updated with oracle_url: None fields.
Order Configuration
crates/settings/src/order.rs, crates/settings/src/yaml/context.rs, crates/settings/src/gui.rs
Introduces oracle_url: Option<String> field to OrderCfg struct with serialization, YAML parsing, default initialization, and equality support; test fixtures updated to include oracle_url initialization.
Quote Generation
crates/quote/src/order_quotes.rs
Adds oracle URL extraction per order and conditional fetching of signed context for IO pairs; populates signedContext dynamically in QuoteV2 structures for both single and batch quote paths.
Order Processing & Accessors
crates/common/src/raindex_client/orders.rs, crates/common/src/raindex_client/order_quotes.rs
Adds oracle_url() accessor on RaindexOrder via parsed_meta inspection; introduces extract_oracle_url static method and wasm binding; propagates oracle_url through get_take_calldata; refactors get_quotes to reuse into_sg_order conversion.
Candidate Building & Signing Context
crates/common/src/take_orders/candidates.rs, crates/common/src/take_orders/config.rs, crates/common/src/test_helpers.rs, crates/common/src/raindex_client/take_orders/single.rs, crates/common/src/raindex_client/take_orders/single_tests.rs
Adds signed_context: Vec<SignedContextV1> field to TakeOrderCandidate; introduces fetch_oracle_for_pair async function; refactors build_take_order_candidates_for_pair to fetch oracle context per order; updates execute_single_take signature with oracle_url parameter; propagates signed_context through candidate building pipeline; test updates add oracle_url parameters and signed_context initialization.
UI Display
packages/ui-components/src/lib/components/detail/OrderDetail.svelte
Adds conditional Oracle block to OrderDetail component displaying oracle URL as clickable link when data.oracleUrl is present.
Dependencies & Submodules
lib/rain.interpreter
Updates rain.interpreter submodule reference.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Quote Client
    participant QG as Quote Generator
    participant Oracle as Oracle Endpoint
    participant OB as Order Book
    
    Client->>QG: get_order_quotes(orders, block_number)
    QG->>QG: extract oracle_url from order metadata
    alt Oracle URL Present
        QG->>QG: encode_oracle_body(order, io_params)
        QG->>Oracle: fetch_signed_context(url, encoded_body)
        Oracle-->>QG: OracleResponse {signer, context, signature}
        QG->>QG: Convert to SignedContextV1
    else No Oracle URL
        QG->>QG: signed_context = []
    end
    QG->>QG: Build QuoteV2 with signed_context
    QG-->>Client: QuoteV2 {..., signedContext}
Loading
sequenceDiagram
    participant Taker as Taker Flow
    participant CB as Candidate Builder
    participant Oracle as Oracle Endpoint
    participant OB as Order Book
    
    Taker->>CB: build_take_order_candidates_for_pair(orders)
    loop For Each Order
        CB->>CB: extract oracle_url from order
        alt Oracle URL Present
            CB->>CB: encode_oracle_body with order params
            CB->>Oracle: fetch_signed_context(url, body)
            Oracle-->>CB: SignedContextV1
        else No Oracle URL
            CB->>CB: signed_context = []
        end
        CB->>CB: try_build_candidate(..., signed_context)
        CB->>CB: TakeOrderCandidate {..., signed_context}
    end
    CB-->>Taker: Vec<TakeOrderCandidate>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: oracle signed context - full stack (Phases 3-7)' accurately reflects the primary objective of this PR, which is to implement oracle signed context functionality across the full stack (backend, frontend, and SDK).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/extract-oracle-url-sdk

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.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/quote/src/order_quotes.rs (1)

67-123: ⚠️ Potential issue | 🟠 Major

Restructure BatchQuoteTarget to batch across all orders, not per-order.

The BatchQuoteTarget multicall is currently invoked inside the per-order loop (line 97), which creates one multicall per order. This contradicts the design intent documented in the retrieved learning: "get_order_quotes() intentionally uses a single BatchQuoteTarget multicall across all orders (rather than one multicall per order) to reduce RPC round-trips and improve performance."

Flatten all pairs across all orders into a single BatchQuoteTarget before calling do_quote().

Regarding oracle context fetching: the current per-pair fetch design is correct and explicitly documented in the function docstring (lines 43–44): "signed context is fetched per IO pair via POST." The batch oracle helpers exist for alternative use cases but are not required here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/quote/src/order_quotes.rs` around lines 67 - 123, The BatchQuoteTarget
multicall is being constructed per-order inside the order loop; instead, in
get_order_quotes gather/flatten all input-output pairs across every order into a
single list (collect pair identifiers and associated metadata from
order_struct/validInputs/validOutputs), build one BatchQuoteTarget for that
flattened list, and call do_quote once with that single BatchQuoteTarget; keep
the existing per-IO-pair signed_context fetch logic
(crate::oracle::fetch_signed_context and encode_oracle_body) unchanged so oracle
contexts remain fetched per pair before assembling the batch request to
do_quote.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/common/src/add_order.rs`:
- Around line 134-146: new_from_deployment() currently always appends a
RaindexSignedContextOracleV1 meta item even if additional_meta already contains
one; update the logic to deduplicate by checking existing meta items (the vector
in additional_meta) for an existing RaindexSignedContextOracleV1 (the same meta
key used by find_in_items()) and either replace that existing item with the one
parsed from deployment.order.oracle_url or remove any existing
RaindexSignedContextOracleV1 entries before pushing the new to_meta_item()
returned by RaindexSignedContextOracleV1::parse; ensure you still return None
when the resulting meta vector is empty.

In `@crates/common/src/raindex_client/orders.rs`:
- Around line 317-319: The wasm impl calls RaindexOrder::extract_oracle_url from
extract_oracle_url_wasm, but extract_oracle_url is currently defined only under
#[cfg(not(target_family = "wasm"))]; move the extract_oracle_url helper out of
the cfg-gated block into an unconditional impl RaindexOrder so both wasm and
non-wasm builds can access it. Concretely, create or expand an impl RaindexOrder
(non-#[cfg]) and place the extract_oracle_url function there (keeping its
signature and behavior), then ensure extract_oracle_url_wasm continues to call
RaindexOrder::extract_oracle_url.

In `@crates/common/src/raindex_client/take_orders/single.rs`:
- Around line 119-130: The warning currently logs the raw oracle_url in the
fetch error branch (see oracle_url variable, crate::oracle::fetch_signed_context
call, and candidate.signed_context assignment); change the log to avoid emitting
sensitive URL components by parsing the URL (e.g., with url::Url::parse) and
logging only non-sensitive parts such as scheme and host (and optionally path)
or a masked/hashed representation, then call tracing::warn! with a message like
"Failed to fetch oracle data" plus the sanitized host/path or mask and the error
(but not the full URL); update the tracing::warn! line accordingly and ensure
any helper masking/parsing logic is unit-tested if added.

In `@crates/common/src/take_orders/candidates.rs`:
- Around line 95-116: The code currently calls fetch_oracle_for_pair with
Address::ZERO inside the quotes loop and binds that result to
TakeOrderCandidate.signed_context, which can produce signatures tied to the
wrong counterparty; instead, remove the preemptive fetch from before
try_build_candidate (do not call fetch_oracle_for_pair with Address::ZERO), call
try_build_candidate with the unpopulated signed_context, and only after
try_build_candidate returns Some(candidate) (and the real taker is known)
perform the oracle fetch using fetch_oracle_for_pair with the actual taker
address and then attach the returned signed_context to the candidate (so
TakeOrderCandidate.signed_context and the later TakeOrderConfigV4.signedContext
are populated with a context signed for the real counterparty). Ensure you only
fetch for candidates that were accepted to avoid unnecessary oracle calls.

In `@crates/quote/src/oracle.rs`:
- Around line 1-257: The file fails rustfmt; run the formatter and commit the
changes so CI passes—run the project formatting command (e.g., nix develop -c
cargo fmt --all or cargo fmt) and stage the updated oracle.rs; ensure you format
the whole crate (including functions like encode_oracle_body,
encode_oracle_body_batch, fetch_signed_context, fetch_signed_context_batch,
extract_oracle_url, and tests) and push the formatted file so the PR no longer
fails CI.
- Around line 125-146: fetch_signed_context_batch currently accepts any response
array length; update it to verify the response length matches the expected
request count to prevent positional mismatches. Modify
fetch_signed_context_batch to accept an expected_count: usize (or derive the
count by deserializing the request body) and after parsing response:
Vec<OracleResponse> check response.len() == expected_count, returning an
OracleError variant (e.g., MismatchedResponseCount with expected and got) when
they differ; ensure the function returns Err(OracleError::...) instead of Ok
when counts mismatch and reference SignedContextV1 and OracleResponse when
mapping on success.

---

Outside diff comments:
In `@crates/quote/src/order_quotes.rs`:
- Around line 67-123: The BatchQuoteTarget multicall is being constructed
per-order inside the order loop; instead, in get_order_quotes gather/flatten all
input-output pairs across every order into a single list (collect pair
identifiers and associated metadata from order_struct/validInputs/validOutputs),
build one BatchQuoteTarget for that flattened list, and call do_quote once with
that single BatchQuoteTarget; keep the existing per-IO-pair signed_context fetch
logic (crate::oracle::fetch_signed_context and encode_oracle_body) unchanged so
oracle contexts remain fetched per pair before assembling the batch request to
do_quote.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23948530-8bce-40d7-9ef3-7e8205d54a1d

📥 Commits

Reviewing files that changed from the base of the PR and between 22af026 and dd752c5.

📒 Files selected for processing (20)
  • crates/common/src/add_order.rs
  • crates/common/src/lib.rs
  • crates/common/src/oracle.rs
  • crates/common/src/parsed_meta.rs
  • crates/common/src/raindex_client/order_quotes.rs
  • crates/common/src/raindex_client/orders.rs
  • crates/common/src/raindex_client/take_orders/single.rs
  • crates/common/src/raindex_client/take_orders/single_tests.rs
  • crates/common/src/take_orders/candidates.rs
  • crates/common/src/take_orders/config.rs
  • crates/common/src/test_helpers.rs
  • crates/quote/Cargo.toml
  • crates/quote/src/lib.rs
  • crates/quote/src/oracle.rs
  • crates/quote/src/order_quotes.rs
  • crates/settings/src/gui.rs
  • crates/settings/src/order.rs
  • crates/settings/src/yaml/context.rs
  • lib/rain.interpreter
  • packages/ui-components/src/lib/components/detail/OrderDetail.svelte

Comment on lines +134 to +146
// If the order has an oracle URL, add a RaindexSignedContextOracleV1 meta item
let additional_meta = {
let mut meta = additional_meta.unwrap_or_default();
if let Some(ref oracle_url) = deployment.order.oracle_url {
let oracle = RaindexSignedContextOracleV1::parse(oracle_url)
.map_err(AddOrderArgsError::RainMetaError)?;
meta.push(oracle.to_meta_item());
}
if meta.is_empty() {
None
} else {
Some(meta)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Deduplicate oracle metadata when the deployment already specifies an oracle URL.

new_from_deployment() now blindly appends a RaindexSignedContextOracleV1 item. If additional_meta already contains one, downstream extractors use find_in_items(), so the effective URL becomes order-dependent instead of letting deployment.order.oracle_url win.

💡 Proposed fix
         let mut meta = additional_meta.unwrap_or_default();
         if let Some(ref oracle_url) = deployment.order.oracle_url {
+            meta.retain(|item| item.magic != KnownMagic::RaindexSignedContextOracleV1);
             let oracle = RaindexSignedContextOracleV1::parse(oracle_url)
                 .map_err(AddOrderArgsError::RainMetaError)?;
             meta.push(oracle.to_meta_item());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/add_order.rs` around lines 134 - 146, new_from_deployment()
currently always appends a RaindexSignedContextOracleV1 meta item even if
additional_meta already contains one; update the logic to deduplicate by
checking existing meta items (the vector in additional_meta) for an existing
RaindexSignedContextOracleV1 (the same meta key used by find_in_items()) and
either replace that existing item with the one parsed from
deployment.order.oracle_url or remove any existing RaindexSignedContextOracleV1
entries before pushing the new to_meta_item() returned by
RaindexSignedContextOracleV1::parse; ensure you still return None when the
resulting meta vector is empty.

Comment on lines +317 to +319
#[wasm_bindgen(js_name = extractOracleUrl)]
pub fn extract_oracle_url_wasm(meta_bytes: &[u8]) -> Option<String> {
RaindexOrder::extract_oracle_url(meta_bytes)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C2 'extract_oracle_url(_wasm)?' crates/common/src/raindex_client/orders.rs
rg -n -C1 '#\[cfg\(target_family = "wasm"\)\]|#\[cfg\(not\(target_family = "wasm"\)\)\]' \
  crates/common/src/raindex_client/orders.rs

Repository: rainlanguage/rain.orderbook

Length of output: 2116


Fix the wasm-only call to a non-wasm helper.

Line 319 calls RaindexOrder::extract_oracle_url(meta_bytes) from the wasm impl block, but the helper is only defined under #[cfg(not(target_family = "wasm"))] at line 403. This causes wasm target compilation to fail because the function is not available. Move extract_oracle_url into an unconditional impl RaindexOrder block so both wasm and non-wasm targets can access it.

🧰 Tools
🪛 GitHub Actions: GitHub Actions Vercel Docs Preview Deployment

[error] 319-319: RaindexOrder::extract_oracle_url not found. The code calls an associated function 'extract_oracle_url', but only 'extract_oracle_url_wasm' (or other variants) may exist. Consider using the correct function name or add the missing function.

🪛 GitHub Actions: GitHub Actions Vercel Preview Deployment

[error] 319-319: RaindexOrder::extract_oracle_url(meta_bytes) not found for struct RaindexOrder. Use associated function RaindexOrder::extract_oracle_url_wasm(meta_bytes) or ensure the correct method exists in this version.

🪛 GitHub Actions: Rainix CI

[error] 319-319: no function or associated item named extract_oracle_url found for struct RaindexOrder in the current scope. Consider using RaindexOrder::extract_oracle_url_wasm as an alternative, or ensure extract_oracle_url is defined for this type.

🪛 GitHub Actions: Test ui-components

[error] 319-319: RaindexOrder::extract_oracle_url not found. The log suggests a similarly named function (extract_oracle_url_wasm) may exist. This indicates a API/API-name mismatch or refactor. Update call to a valid associated function.

🪛 GitHub Actions: Test webapp

[error] 319-319: RaindexOrder::extract_oracle_url(meta_bytes) not found. Suggests using RaindexOrder::extract_oracle_url_wasm(meta_bytes) or other existing associated constructors.


[error] 319-319: Compilation failed due to the above error while building rain_orderbook_common (lib).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/raindex_client/orders.rs` around lines 317 - 319, The wasm
impl calls RaindexOrder::extract_oracle_url from extract_oracle_url_wasm, but
extract_oracle_url is currently defined only under #[cfg(not(target_family =
"wasm"))]; move the extract_oracle_url helper out of the cfg-gated block into an
unconditional impl RaindexOrder so both wasm and non-wasm builds can access it.
Concretely, create or expand an impl RaindexOrder (non-#[cfg]) and place the
extract_oracle_url function there (keeping its signature and behavior), then
ensure extract_oracle_url_wasm continues to call
RaindexOrder::extract_oracle_url.

Comment on lines +119 to +130
if let Some(url) = oracle_url {
let body = crate::oracle::encode_oracle_body(
&candidate.order,
candidate.input_io_index,
candidate.output_io_index,
taker,
);
match crate::oracle::fetch_signed_context(&url, body).await {
Ok(ctx) => candidate.signed_context = vec![ctx],
Err(e) => {
tracing::warn!("Failed to fetch oracle data from {}: {}", url, e);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't emit raw oracle URLs in warning logs.

Oracle endpoints can carry credentials or signed query params, and this branch logs the URL verbatim on every fetch failure. Avoid logging the raw URL here; even the formatted error can repeat it.

💡 Proposed fix
         match crate::oracle::fetch_signed_context(&url, body).await {
             Ok(ctx) => candidate.signed_context = vec![ctx],
-            Err(e) => {
-                tracing::warn!("Failed to fetch oracle data from {}: {}", url, e);
-            }
+            Err(_) => {
+                tracing::warn!("Failed to fetch oracle data");
+            }
         }
📝 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
if let Some(url) = oracle_url {
let body = crate::oracle::encode_oracle_body(
&candidate.order,
candidate.input_io_index,
candidate.output_io_index,
taker,
);
match crate::oracle::fetch_signed_context(&url, body).await {
Ok(ctx) => candidate.signed_context = vec![ctx],
Err(e) => {
tracing::warn!("Failed to fetch oracle data from {}: {}", url, e);
}
if let Some(url) = oracle_url {
let body = crate::oracle::encode_oracle_body(
&candidate.order,
candidate.input_io_index,
candidate.output_io_index,
taker,
);
match crate::oracle::fetch_signed_context(&url, body).await {
Ok(ctx) => candidate.signed_context = vec![ctx],
Err(_) => {
tracing::warn!("Failed to fetch oracle data");
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/raindex_client/take_orders/single.rs` around lines 119 -
130, The warning currently logs the raw oracle_url in the fetch error branch
(see oracle_url variable, crate::oracle::fetch_signed_context call, and
candidate.signed_context assignment); change the log to avoid emitting sensitive
URL components by parsing the URL (e.g., with url::Url::parse) and logging only
non-sensitive parts such as scheme and host (and optionally path) or a
masked/hashed representation, then call tracing::warn! with a message like
"Failed to fetch oracle data" plus the sanitized host/path or mask and the error
(but not the full URL); update the tracing::warn! line accordingly and ensure
any helper masking/parsing logic is unit-tested if added.

Comment on lines +95 to +116
for quote in &quotes {
let signed_context = match &oracle_url {
Some(url) => {
fetch_oracle_for_pair(
url,
&order_v4,
quote.pair.input_index,
quote.pair.output_index,
Address::ZERO, // counterparty unknown at candidate building time
)
.await
}
None => vec![],
};

if let Some(candidate) = try_build_candidate(
orderbook,
&order_v4,
quote,
input_token,
output_token,
signed_context,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C2 'get_quotes\(|fetch_oracle_for_pair|Address::ZERO|signed_context' \
  crates/common/src/take_orders/candidates.rs \
  crates/common/src/take_orders/config.rs \
  crates/common/src/raindex_client/take_orders/single.rs

Repository: rainlanguage/rain.orderbook

Length of output: 5575


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if order.get_quotes() makes oracle calls
echo "=== Searching for order.get_quotes implementation ==="
rg -n 'fn get_quotes|async.*get_quotes' --type rust

echo ""
echo "=== Checking if get_quotes calls fetch_oracle or similar ==="
rg -B5 -A15 'fn get_quotes' crates/common/src/ --type rust | head -80

echo ""
echo "=== Checking candidates lifecycle - where are they consumed ==="
rg -n 'build_take_order_candidates' crates/common/src/ --type rust

Repository: rainlanguage/rain.orderbook

Length of output: 2845


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full get_quotes implementation ==="
wc -l crates/common/src/raindex_client/order_quotes.rs
sed -n '109,180p' crates/common/src/raindex_client/order_quotes.rs

echo ""
echo "=== Check if get_quotes calls oracle or fetch_signed_context ==="
sed -n '109,200p' crates/common/src/raindex_client/order_quotes.rs | grep -i 'oracle\|fetch_signed\|fetch_oracle' || echo "No oracle calls found in get_quotes"

echo ""
echo "=== Check how build_take_order_candidates_for_pair is used in selection.rs ==="
cat -n crates/common/src/raindex_client/take_orders/selection.rs | head -50

echo ""
echo "=== Check full context of candidates usage ==="
sed -n '1,100p' crates/common/src/raindex_client/take_orders/selection.rs

Repository: rainlanguage/rain.orderbook

Length of output: 9711


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify single.rs pattern more carefully ==="
sed -n '50,135p' crates/common/src/raindex_client/take_orders/single.rs

echo ""
echo "=== Check what happens with signed_context in candidate vs single take ==="
rg -B3 -A3 'signed_context.*vec!\[\]' crates/common/src/ --type rust

Repository: rainlanguage/rain.orderbook

Length of output: 3599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if candidates from build_take_order_candidates_for_pair flow to execute_single_take ==="
rg -n 'execute_single_take' crates/common/src/ --type rust | head -20

echo ""
echo "=== See how candidates are passed between functions ==="
rg -B5 -A10 'execute_single_take' crates/common/src/raindex_client/take_orders/ --type rust | head -80

Repository: rainlanguage/rain.orderbook

Length of output: 9275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check orders.rs execute_single_take call at line 694 ==="
sed -n '680,720p' crates/common/src/raindex_client/orders.rs

echo ""
echo "=== Check what candidate is being passed ==="
sed -n '650,700p' crates/common/src/raindex_client/orders.rs

echo ""
echo "=== Full context of execute_single_take call ==="
sed -n '600,750p' crates/common/src/raindex_client/orders.rs

Repository: rainlanguage/rain.orderbook

Length of output: 9167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check selection.rs return and how result is used ==="
sed -n '43,120p' crates/common/src/raindex_client/take_orders/selection.rs

echo ""
echo "=== Check what happens after select_best_orderbook_simulation ==="
rg -n 'select_best_orderbook_simulation\|SimulationResult' crates/common/src/raindex_client/ --type rust | head -30

echo ""
echo "=== Check if SimulationResult is passed to config building ==="
rg -B3 -A3 'build_take_orders_config_from_simulation\|SimulationResult.*config' crates/common/src/ --type rust | head -60

Repository: rainlanguage/rain.orderbook

Length of output: 2668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find where SimulationResult is used after selection ==="
rg -n 'SimulationResult' crates/common/src/raindex_client/take_orders/ --type rust | head -20

echo ""
echo "=== Find build_take_orders_config_from_simulation calls ==="
rg -B5 -A15 'build_take_orders_config_from_simulation' crates/common/src/ --type rust

echo ""
echo "=== Check if build_candidates_for_chain result is used for multi-order execution ==="
rg -B10 -A20 'build_candidates_for_chain' crates/common/src/raindex_client/ --type rust | head -100

Repository: rainlanguage/rain.orderbook

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify that oracle context from Address::ZERO actually gets into the transaction ==="
sed -n '86,100p' crates/common/src/take_orders/config.rs

echo ""
echo "=== Check how signedContext is used in the transaction ==="
rg -B2 -A2 'signedContext' crates/common/src/ --type rust | head -40

echo ""
echo "=== Verify the actual lifecycle: multi-order path with oracle ==="
sed -n '66,140p' crates/common/src/raindex_client/take_orders/mod.rs

Repository: rainlanguage/rain.orderbook

Length of output: 6029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check what oracle_url is passed to build_take_order_candidates_for_pair ==="
sed -n '12,25p' crates/common/src/raindex_client/take_orders/selection.rs

echo ""
echo "=== Check the function signature of build_take_order_candidates_for_pair ==="
sed -n '59,75p' crates/common/src/take_orders/candidates.rs

echo ""
echo "=== Check if there's another path that passes oracle_url ==="
rg -n 'build_take_order_candidates_for_pair' crates/common/src/ --type rust

Repository: rainlanguage/rain.orderbook

Length of output: 1953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify oracle_url comes from order object ==="
rg -n 'fn oracle_url\|oracle_url:' crates/common/src/raindex_client/orders.rs | head -10

echo ""
echo "=== Check if oracle is actually used in the multi-order path ==="
sed -n '85,125p' crates/common/src/take_orders/candidates.rs

echo ""
echo "=== Verify address matching in the actual contract interaction ==="
rg -B3 -A3 'counterparty.*taker\|taker.*counterparty' crates/common/src/ --type rust | head -30

Repository: rainlanguage/rain.orderbook

Length of output: 120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look for oracle_url in candidates.rs ==="
sed -n '59,125p' crates/common/src/take_orders/candidates.rs

echo ""
echo "=== Check if oracle_url is used anywhere in candidates.rs ==="
grep -n 'oracle_url' crates/common/src/take_orders/candidates.rs

echo ""
echo "=== Check what parameters the function actually accepts ==="
sed -n '59,85p' crates/common/src/take_orders/candidates.rs

Repository: rainlanguage/rain.orderbook

Length of output: 3791


Don't bind candidate oracle context to Address::ZERO.

Lines 96–105 encode the oracle request with Address::ZERO, then line 116 persists that result on the candidate. TakeOrderCandidate.signed_context is later forwarded into TakeOrderConfigV4.signedContext (config.rs:94), so any oracle that includes counterparty in its signature will be bound to the wrong address. The single-take path fetches with the real taker after the candidate is formed; this path should defer the oracle fetch until the taker is known and only after the quote passes try_build_candidate, which also avoids unnecessary fetches on quotes that may be discarded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/take_orders/candidates.rs` around lines 95 - 116, The code
currently calls fetch_oracle_for_pair with Address::ZERO inside the quotes loop
and binds that result to TakeOrderCandidate.signed_context, which can produce
signatures tied to the wrong counterparty; instead, remove the preemptive fetch
from before try_build_candidate (do not call fetch_oracle_for_pair with
Address::ZERO), call try_build_candidate with the unpopulated signed_context,
and only after try_build_candidate returns Some(candidate) (and the real taker
is known) perform the oracle fetch using fetch_oracle_for_pair with the actual
taker address and then attach the returned signed_context to the candidate (so
TakeOrderCandidate.signed_context and the later TakeOrderConfigV4.signedContext
are populated with a context signed for the real counterparty). Ensure you only
fetch for candidates that were accepted to avoid unnecessary oracle calls.

Comment on lines +1 to +257
use alloy::primitives::{Address, Bytes, FixedBytes, U256};
use alloy::sol_types::SolValue;
use rain_orderbook_bindings::IOrderBookV6::{OrderV4, SignedContextV1};
use rain_orderbook_subgraph_client::types::common::SgOrder;
use reqwest::Client;
use serde::{Deserialize, Serialize};

/// Error types for oracle fetching
#[derive(Debug, thiserror::Error)]
pub enum OracleError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),

#[error("Invalid oracle response: {0}")]
InvalidResponse(String),

#[error("Invalid URL: {0}")]
InvalidUrl(String),
}

/// JSON response format from an oracle endpoint.
/// Maps directly to `SignedContextV1` in the orderbook contract.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OracleResponse {
/// The signer address (EIP-191 signer of the context data)
pub signer: Address,
/// The signed context data as bytes32[] values
pub context: Vec<FixedBytes<32>>,
/// The EIP-191 signature over keccak256(abi.encodePacked(context))
pub signature: Bytes,
}

impl From<OracleResponse> for SignedContextV1 {
fn from(resp: OracleResponse) -> Self {
SignedContextV1 {
signer: resp.signer,
context: resp.context,
signature: resp.signature,
}
}
}

/// Encode the POST body for a single oracle request.
///
/// The body is `abi.encode(OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)`.
pub fn encode_oracle_body(
order: &OrderV4,
input_io_index: u32,
output_io_index: u32,
counterparty: Address,
) -> Vec<u8> {
(
order.clone(),
U256::from(input_io_index),
U256::from(output_io_index),
counterparty,
)
.abi_encode()
}

/// Encode the POST body for a batch oracle request.
///
/// The body is `abi.encode((OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[])`.
pub fn encode_oracle_body_batch(
requests: Vec<(&OrderV4, u32, u32, Address)>,
) -> Vec<u8> {
let tuples: Vec<_> = requests
.into_iter()
.map(|(order, input_io_index, output_io_index, counterparty)| {
(
order.clone(),
U256::from(input_io_index),
U256::from(output_io_index),
counterparty,
)
})
.collect();

tuples.abi_encode()
}

/// Fetch signed context from an oracle endpoint via POST (single request).
///
/// The endpoint receives an ABI-encoded body containing the order details
/// that will be used for calculateOrderIO:
/// `abi.encode(OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)`
///
/// The endpoint must respond with a JSON body matching a single `OracleResponse`.
///
/// NOTE: This is a legacy function. The batch format is preferred.
pub async fn fetch_signed_context(
url: &str,
body: Vec<u8>,
) -> Result<SignedContextV1, OracleError> {
let builder = Client::builder();
#[cfg(not(target_family = "wasm"))]
let builder = builder.timeout(std::time::Duration::from_secs(10));
let client = builder.build()?;

// For single requests, we still expect a JSON array response but with one item
let response: Vec<OracleResponse> = client
.post(url)
.header("Content-Type", "application/octet-stream")
.body(body)
.send()
.await?
.error_for_status()?
.json()
.await?;

if response.len() != 1 {
return Err(OracleError::InvalidResponse(
format!("Expected 1 response, got {}", response.len())
));
}

Ok(response.into_iter().next().unwrap().into())
}

/// Fetch signed context from an oracle endpoint via POST (batch request).
///
/// The endpoint receives an ABI-encoded body containing an array of order details:
/// `abi.encode((OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[])`
///
/// The endpoint must respond with a JSON array of `OracleResponse` objects.
/// The response array length must match the request array length.
pub async fn fetch_signed_context_batch(
url: &str,
body: Vec<u8>,
) -> Result<Vec<SignedContextV1>, OracleError> {
let builder = Client::builder();
#[cfg(not(target_family = "wasm"))]
let builder = builder.timeout(std::time::Duration::from_secs(10));
let client = builder.build()?;

let response: Vec<OracleResponse> = client
.post(url)
.header("Content-Type", "application/octet-stream")
.body(body)
.send()
.await?
.error_for_status()?
.json()
.await?;

Ok(response.into_iter().map(|resp| resp.into()).collect())
}

/// Extract the oracle URL from an SgOrder's meta, if present.
///
/// Parses the meta bytes and looks for a `RaindexSignedContextOracleV1` entry.
/// Returns `None` if meta is absent, unparseable, or doesn't contain an oracle entry.
pub fn extract_oracle_url(order: &SgOrder) -> Option<String> {
use rain_metadata::types::raindex_signed_context_oracle::RaindexSignedContextOracleV1;
use rain_metadata::RainMetaDocumentV1Item;

let meta = order.meta.as_ref()?;
let decoded = alloy::hex::decode(&meta.0).ok()?;
let items = RainMetaDocumentV1Item::cbor_decode(&decoded).ok()?;
let oracle = RaindexSignedContextOracleV1::find_in_items(&items).ok()??;
Some(oracle.url().to_string())
}

#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::{address, FixedBytes};
use rain_orderbook_bindings::IOrderBookV6::{EvaluableV4, IOV2, OrderV4};

#[test]
fn test_oracle_response_to_signed_context() {
let ctx_val = FixedBytes::<32>::from([0x2a; 32]);
let response = OracleResponse {
signer: address!("0x1234567890123456789012345678901234567890"),
context: vec![ctx_val],
signature: Bytes::from(vec![0xaa, 0xbb, 0xcc]),
};

let signed: SignedContextV1 = response.into();
assert_eq!(
signed.signer,
address!("0x1234567890123456789012345678901234567890")
);
assert_eq!(signed.context.len(), 1);
assert_eq!(signed.context[0], ctx_val);
assert_eq!(signed.signature, Bytes::from(vec![0xaa, 0xbb, 0xcc]));
}

#[test]
fn test_encode_oracle_body_single() {
let order = create_test_order();
let body = encode_oracle_body(&order, 1, 2, address!("0x1111111111111111111111111111111111111111"));
assert!(!body.is_empty());
}

#[test]
fn test_encode_oracle_body_batch() {
let order1 = create_test_order();
let order2 = create_test_order();

let requests = vec![
(&order1, 1, 2, address!("0x1111111111111111111111111111111111111111")),
(&order2, 3, 4, address!("0x2222222222222222222222222222222222222222")),
];

let body = encode_oracle_body_batch(requests);
assert!(!body.is_empty());

// Batch encoding should be different from single encoding
let single_body = encode_oracle_body(&order1, 1, 2, address!("0x1111111111111111111111111111111111111111"));
assert_ne!(body, single_body);
}

#[tokio::test]
async fn test_fetch_signed_context_invalid_url() {
let result = fetch_signed_context("not-a-url", vec![]).await;
assert!(result.is_err());
}

#[tokio::test]
async fn test_fetch_signed_context_unreachable() {
let result = fetch_signed_context("http://127.0.0.1:1/oracle", vec![]).await;
assert!(result.is_err());
}

#[tokio::test]
async fn test_fetch_signed_context_batch_invalid_url() {
let result = fetch_signed_context_batch("not-a-url", vec![]).await;
assert!(result.is_err());
}

#[tokio::test]
async fn test_fetch_signed_context_batch_unreachable() {
let result = fetch_signed_context_batch("http://127.0.0.1:1/oracle", vec![]).await;
assert!(result.is_err());
}

fn create_test_order() -> OrderV4 {
OrderV4 {
owner: address!("0x0000000000000000000000000000000000000000"),
evaluable: EvaluableV4 {
interpreter: address!("0x0000000000000000000000000000000000000000"),
store: address!("0x0000000000000000000000000000000000000000"),
bytecode: Bytes::new(),
},
validInputs: vec![IOV2 {
token: address!("0x0000000000000000000000000000000000000000"),
vaultId: FixedBytes::<32>::ZERO,
}],
validOutputs: vec![IOV2 {
token: address!("0x0000000000000000000000000000000000000000"),
vaultId: FixedBytes::<32>::ZERO,
}],
nonce: FixedBytes::<32>::ZERO,
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Run rustfmt on this file before merge.

Rainix CI is already failing on oracle.rs formatting, so this keeps the PR red as-is.

As per coding guidelines, "Format Rust code with nix develop -c cargo fmt --all."

🧰 Tools
🪛 GitHub Actions: Rainix CI

[error] 1-999: Rustfmt differences detected in oracle.rs. Run 'cargo fmt' to fix code formatting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/quote/src/oracle.rs` around lines 1 - 257, The file fails rustfmt; run
the formatter and commit the changes so CI passes—run the project formatting
command (e.g., nix develop -c cargo fmt --all or cargo fmt) and stage the
updated oracle.rs; ensure you format the whole crate (including functions like
encode_oracle_body, encode_oracle_body_batch, fetch_signed_context,
fetch_signed_context_batch, extract_oracle_url, and tests) and push the
formatted file so the PR no longer fails CI.

Comment on lines +125 to +146
/// The endpoint must respond with a JSON array of `OracleResponse` objects.
/// The response array length must match the request array length.
pub async fn fetch_signed_context_batch(
url: &str,
body: Vec<u8>,
) -> Result<Vec<SignedContextV1>, OracleError> {
let builder = Client::builder();
#[cfg(not(target_family = "wasm"))]
let builder = builder.timeout(std::time::Duration::from_secs(10));
let client = builder.build()?;

let response: Vec<OracleResponse> = client
.post(url)
.header("Content-Type", "application/octet-stream")
.body(body)
.send()
.await?
.error_for_status()?
.json()
.await?;

Ok(response.into_iter().map(|resp| resp.into()).collect())
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject batch responses with the wrong item count.

This helper documents a 1:1 positional response contract, but it currently treats any array length as success. A truncated or extra oracle response can then shift signed contexts onto the wrong request downstream.

💡 Proposed fix
 pub async fn fetch_signed_context_batch(
     url: &str,
     body: Vec<u8>,
+    expected_len: usize,
 ) -> Result<Vec<SignedContextV1>, OracleError> {
@@
     let response: Vec<OracleResponse> = client
         .post(url)
@@
         .json()
         .await?;
 
-    Ok(response.into_iter().map(|resp| resp.into()).collect())
+    if response.len() != expected_len {
+        return Err(OracleError::InvalidResponse(format!(
+            "Expected {} responses, got {}",
+            expected_len,
+            response.len()
+        )));
+    }
+
+    Ok(response.into_iter().map(Into::into).collect())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/quote/src/oracle.rs` around lines 125 - 146,
fetch_signed_context_batch currently accepts any response array length; update
it to verify the response length matches the expected request count to prevent
positional mismatches. Modify fetch_signed_context_batch to accept an
expected_count: usize (or derive the count by deserializing the request body)
and after parsing response: Vec<OracleResponse> check response.len() ==
expected_count, returning an OracleError variant (e.g., MismatchedResponseCount
with expected and got) when they differ; ensure the function returns
Err(OracleError::...) instead of Ok when counts mismatch and reference
SignedContextV1 and OracleResponse when mapping on success.

@hardyjosh
Copy link
Contributor Author

Superseded by fresh branch rebased on current main

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.

1 participant