feat: oracle signed context - full stack (Phases 3-7)#2503
feat: oracle signed context - full stack (Phases 3-7)#2503
Conversation
…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)
WalkthroughThis 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
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}
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>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorRestructure
BatchQuoteTargetto batch across all orders, not per-order.The
BatchQuoteTargetmulticall 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
BatchQuoteTargetbefore callingdo_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
📒 Files selected for processing (20)
crates/common/src/add_order.rscrates/common/src/lib.rscrates/common/src/oracle.rscrates/common/src/parsed_meta.rscrates/common/src/raindex_client/order_quotes.rscrates/common/src/raindex_client/orders.rscrates/common/src/raindex_client/take_orders/single.rscrates/common/src/raindex_client/take_orders/single_tests.rscrates/common/src/take_orders/candidates.rscrates/common/src/take_orders/config.rscrates/common/src/test_helpers.rscrates/quote/Cargo.tomlcrates/quote/src/lib.rscrates/quote/src/oracle.rscrates/quote/src/order_quotes.rscrates/settings/src/gui.rscrates/settings/src/order.rscrates/settings/src/yaml/context.rslib/rain.interpreterpackages/ui-components/src/lib/components/detail/OrderDetail.svelte
| // 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) | ||
| } |
There was a problem hiding this comment.
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.
| #[wasm_bindgen(js_name = extractOracleUrl)] | ||
| pub fn extract_oracle_url_wasm(meta_bytes: &[u8]) -> Option<String> { | ||
| RaindexOrder::extract_oracle_url(meta_bytes) |
There was a problem hiding this comment.
🧩 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.rsRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| for quote in "es { | ||
| 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, |
There was a problem hiding this comment.
🧩 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.rsRepository: 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 rustRepository: 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.rsRepository: 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 rustRepository: 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 -80Repository: 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.rsRepository: 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 -60Repository: 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 -100Repository: 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.rsRepository: 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 rustRepository: 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 -30Repository: 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.rsRepository: 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.
| 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, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| /// 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()) |
There was a problem hiding this comment.
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.
|
Superseded by fresh branch rebased on current main |
Merges the full oracle signed context stack into main. This includes:
oracle-urlfrom dotrain order config and encodes into order metadataextractOracleUrlstatic method to RaindexOrderThese 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-urlin 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