Skip to content

fix: quote via orderbook-native multicall, not Multicall3#2550

Open
hardyjosh wants to merge 1 commit intomainfrom
2026-04-15-quote-counterparty-msg-sender
Open

fix: quote via orderbook-native multicall, not Multicall3#2550
hardyjosh wants to merge 1 commit intomainfrom
2026-04-15-quote-counterparty-msg-sender

Conversation

@hardyjosh
Copy link
Copy Markdown
Contributor

@hardyjosh hardyjosh commented Apr 15, 2026

Motivation

OrderBookV6.quote2 passes msg.sender as the counterparty into calculateOrderIO. The quote crate batched via Multicall3 (provider.multicall().aggregate3()), which forwards via CALL — so msg.sender inside quote2 was always the Multicall3 contract, never the intended taker.

For public strategies this didn't matter. For strategies that inspect msg.sender — e.g. API-gated strategies asserting signed-context<0 0>() == order-counterparty() (see the companion st0x.rest.api PR) — every quote reverted, the candidate was silently dropped as a failed quote, and the gated order never entered selection even though its actual takeOrders4 path (which uses msg.sender = taker) worked fine.

Relates to the parent PR #2547, which introduced SignedContextInjector and already threads a counterparty through the quote pipeline. This change makes the counterparty actually arrive at quote2 as msg.sender.

Solution

OrderBookV6 inherits OpenZeppelin's Multicall, which uses delegatecall to self — preserving msg.sender through the batch. Calling orderbook.multicall([quote2,...]) via eth_call with from=counterparty gives each inner quote2 the intended msg.sender. One RPC per orderbook, correct semantics — we keep batching without the unsound Multicall3 layer.

  • quote_chunk_once takes counterparty: Address, groups targets by orderbook, ABI-encodes each quote2 and wraps in a single orderbook.multicall(bytes[]) eth_call per group with from=counterparty.
  • Per-orderbook groups run concurrently via futures::future::join_all.
  • Chunk-level reverts flow through a new Error::ChunkReverted and drive the existing probe-and-split bisection to attribute reverts to singletons when OZ Multicall bubbles the first delegatecall revert.
  • multicall_address plumbing removed from the quote pipeline — the Multicall3 override address no longer applies here. Unchanged in unrelated ERC20 metadata reads.
  • counterparty: Address threaded through batch_quote, BatchQuoteTarget::do_quote, and get_order_quotes. The wasm-exported get_order_quotes_batch stays backward-compat via Address::ZERO; the injector-aware variant already has the counterparty in scope.
  • Tests rewritten from Multicall3 mocks to orderbook OZ-multicall mocks, including one asserting the eth_call from is the counterparty.

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Verified locally:

  • cargo check -p rain_orderbook_quote / -p rain_orderbook_common clean
  • cargo test -p rain_orderbook_quote --lib — 51/51
  • cargo test -p rain_orderbook_common --lib — 898/898
  • cargo fmt

Summary by CodeRabbit

  • Documentation
    • Updated RPC batching architecture documentation.
  • New Features
    • Added required --counterparty CLI option (replaces previous multicall option) to set the source address for quote simulations.
  • Bug Fixes
    • Improved error attribution for failed quote requests with chunk-level revert handling and clearer transport error reporting.
  • Tests
    • Updated test mocks and suites to reflect the new multicall-bytes[] behavior and counterparty usage.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

The PR replaces Multicall3 aggregate3 batching with OpenZeppelin multicall(bytes[]), makes counterparty: Address the required from for inner eth_calls (replacing multicall_address), changes error surface to include chunk-level reverts and transport errors, updates decoding to bytes[] per-target returns, and updates tests and mocks accordingly.

Changes

Cohort / File(s) Summary
Core Quoting API
crates/quote/src/quote.rs, crates/quote/src/rpc.rs
Replaced multicall_address: Option<Address> with counterparty: Address across public do_quote APIs and batch_quote. Switched batching from Multicall3 aggregate3 to OZ multicall(bytes[]); inner calls set from = counterparty. Implemented chunk-level bisection for chunk reverts and per-element bytes[] decoding mapping decode errors to FailedQuote::CorruptReturnData and exists=false to FailedQuote::NonExistent.
Error Handling
crates/quote/src/error.rs
Added Error::ChunkReverted(Box<FailedQuote>) and Error::TransportError(String) variants to represent chunk-level multicall reverts and raw RPC transport failures.
CLI / Wiring
crates/quote/src/cli/mod.rs
Replaced multicall_address: Option<Address> with counterparty: Address (default Address::ZERO) and threaded counterparty into both Target and Spec quote paths; updated unit tests to pass Address::ZERO.
Order Quotes Logic
crates/quote/src/order_quotes.rs, crates/common/src/raindex_client/order_quotes.rs
get_order_quotes now preserves input order using all_responses: Vec<Option<...>> and scatters RPC quote results back into original slots; BatchQuoteTarget::do_quote now receives counterparty. Tests/mocks updated to encode multicall bytes[]; removed Solidity-typed Result helper and added encode_multicall_bytes(inner: Vec<quoteReturn>) -> String.
Tests / Fixtures (JS & Rust)
packages/orderbook/test/js_api/raindexClient.test.ts, crates/common/src/raindex_client/...
Updated mocked JSON-RPC multicall payloads to OZ-style bytes[] encoding with per-element ABI-encoded quote2Return tuples (single- and multi-element cases). Adjusted unit tests to expect single-element success where applicable and updated error expectation styles (per-target failures).
Documentation
crates/quote/ARCHITECTURE.md
Documented shift from Multicall3 aggregate3 to OZ multicall(bytes[]), new counterparty CLI option, chunk-level bisection behavior, and changes to error attribution and decoding.

Sequence Diagram(s)

sequenceDiagram
    actor Caller
    participant QuoteAPI as Quote API<br/>(quote.rs)
    participant RPC as RPC Layer<br/>(rpc.rs)
    participant Orderbook as Orderbook<br/>Multicall
    participant ErrorReg as ErrorRegistry

    Caller->>QuoteAPI: do_quote(counterparty)
    QuoteAPI->>RPC: batch_quote(targets, counterparty)
    RPC->>RPC: Group targets by orderbook and chunk them
    RPC->>Orderbook: eth_call multicall(bytes[]) (from=counterparty)
    alt Multicall Succeeds
        Orderbook-->>RPC: returns bytes[] (per-target ABI payloads)
        RPC->>RPC: Decode each bytes element → quote2Return
        RPC->>RPC: Map exists=false → FailedQuote::NonExistent
        RPC->>RPC: Map decode failures → FailedQuote::CorruptReturnData
    else Chunk Reverts
        Orderbook-->>RPC: revert with error data
        RPC->>ErrorReg: decode revert selector
        ErrorReg-->>RPC: known/unknown error
        RPC->>RPC: Bisect chunk and repeat eth_call per slice until singletons
        alt Singleton Reverts
            RPC->>RPC: attribute revert → per-target FailedQuote
        else Singleton Succeeds
            RPC->>RPC: decode → per-target quote2Return
        end
    end
    RPC-->>QuoteAPI: scatter results preserving input order
    QuoteAPI-->>Caller: Vec<QuoteResult>
Loading
sequenceDiagram
    participant RPC as RPC Layer
    participant MC3 as Multicall3<br/>aggregate3()

    RPC->>MC3: aggregate3([calls...])
    alt All Succeed
        MC3-->>RPC: Vec<Result {success, returnData}>
    else Any Fails
        MC3-->>RPC: Partial results with failure indicators
    end
    RPC->>RPC: Decode returnData per element and infer errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through bytes, a curious feat,
From aggregate3 to OZ's neat beat.
Counterparty whispers, from set true,
I bisected reverts till the culprit I knew.
Now quotes align in tidy rows—hooray!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing Multicall3-based quote batching with orderbook-native OZ multicall to preserve msg.sender context.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-04-15-quote-counterparty-msg-sender

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.

hardyjosh pushed a commit to ST0x-Technology/st0x.rest.api that referenced this pull request Apr 15, 2026
Picks up the quote RPC refactor in rainlanguage/raindex#2550: quotes
now use orderbook-native `multicall(bytes[])` with
`from=counterparty`, preserving `msg.sender` through the batch. This
is what lets API-gated strategies (whose `calculate-io` asserts
`signed-context<0 0>() == order-counterparty()`) actually survive the
quote stage and enter the take-orders selection instead of being
silently filtered.

No code changes in this repo — the counterparty was already threaded
through to the rain.orderbook quote layer via the injector flow in
the parent PR; it just wasn't being used by the RPC layer until the
submodule fix.

Depends on rainlanguage/raindex#2550.
@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from 7f6741f to 85bb758 Compare April 15, 2026 12:42
hardyjosh pushed a commit to ST0x-Technology/st0x.rest.api that referenced this pull request Apr 15, 2026
Picks up the quote RPC refactor in rainlanguage/raindex#2550: quotes
now use orderbook-native `multicall(bytes[])` with
`from=counterparty`, preserving `msg.sender` through the batch. This
is what lets API-gated strategies (whose `calculate-io` asserts
`signed-context<0 0>() == order-counterparty()`) actually survive the
quote stage and enter the take-orders selection instead of being
silently filtered.

No code changes in this repo — the counterparty was already threaded
through to the rain.orderbook quote layer via the injector flow in
the parent PR; it just wasn't being used by the RPC layer until the
submodule fix.

Depends on rainlanguage/raindex#2550.
@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from 85bb758 to 4dfef89 Compare April 15, 2026 13:45
hardyjosh pushed a commit to ST0x-Technology/st0x.rest.api that referenced this pull request Apr 15, 2026
Picks up the quote RPC refactor in rainlanguage/raindex#2550: quotes
now use orderbook-native `multicall(bytes[])` with
`from=counterparty`, preserving `msg.sender` through the batch. This
is what lets API-gated strategies (whose `calculate-io` asserts
`signed-context<0 0>() == order-counterparty()`) actually survive the
quote stage and enter the take-orders selection instead of being
silently filtered.

No code changes in this repo — the counterparty was already threaded
through to the rain.orderbook quote layer via the injector flow in
the parent PR; it just wasn't being used by the RPC layer until the
submodule fix.

Depends on rainlanguage/raindex#2550.
@hardyjosh hardyjosh marked this pull request as ready for review April 15, 2026 15:33
@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from 4dfef89 to c10ecf1 Compare April 15, 2026 15:38
@hardyjosh
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (1)
crates/quote/src/order_quotes.rs (1)

250-256: Avoid a release-only panic if the scatter invariant is ever broken.

debug_assert_eq! disappears in release builds, but unwrap() does not. If a future change in BatchQuoteTarget::do_quote() or the scatter logic returns fewer entries than quoted_slot_indices, this path will panic instead of surfacing a normal Error.

🤖 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 250 - 256, The code currently
uses debug_assert_eq!(quote_results.len(), quoted_slot_indices.len()) and later
unwrap() which can panic in release if the invariant breaks; replace the
debug-only assert with a runtime check: if quote_results.len() !=
quoted_slot_indices.len() return an Err that surfaces a descriptive error
(mentioning BatchQuoteTarget::do_quote(), quote_results and quoted_slot_indices)
before scattering into all_responses, then perform the scatter and safely
collect the options (or return Err if any slot remains None). Ensure you update
the function's error return path to propagate this runtime check instead of
allowing unwrap()-based panics.
🤖 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/quote/ARCHITECTURE.md`:
- Around line 100-104: Update the ARCHITECTURE.md revert-attribution paragraph
to reflect that quote_chunk_with_probe_and_split always performs bisection (no
probe stage or whole-chunk shortcut anymore): remove references to the "probe"
stage and the "if no probe succeeds, the whole chunk gets the same decoded
failure" branch, and replace step 5 with a concise description that the function
recursively halves the chunk until batch-of-1 failures are isolated and
attributed to specific QuoteTarget entries; leave the note about orderbook
groups running via futures::future::join_all unchanged.

In `@crates/quote/src/rpc.rs`:
- Around line 129-155: The match currently wraps any RpcError (including
Transport/SerError/DeserError/NullResp/UnsupportedFeature/LocalUsageError) and
ErrorResp-without-revert as
Error::ChunkReverted(FailedQuote::CorruptReturnData), which hides infrastructure
failures; change the logic so only ErrorResp with revert bytes is converted into
a per-target FailedQuote and wrapped as Error::ChunkReverted(Box::new(...))
(using as_revert_data + AbiDecodedErrorType::selector_registry_abi_decode ->
FailedQuote::RevertError / RevertErrorDecodeFailed), while all other RpcError
variants (the Err(e) arm and the ErrorResp branch when as_revert_data() is None)
should be returned as an infrastructure-level error (propagate/convert to an
appropriate non-ChunkReverted Error variant) so RPC/transport/serialization
failures are not treated as chunk bisection failures.

---

Nitpick comments:
In `@crates/quote/src/order_quotes.rs`:
- Around line 250-256: The code currently uses
debug_assert_eq!(quote_results.len(), quoted_slot_indices.len()) and later
unwrap() which can panic in release if the invariant breaks; replace the
debug-only assert with a runtime check: if quote_results.len() !=
quoted_slot_indices.len() return an Err that surfaces a descriptive error
(mentioning BatchQuoteTarget::do_quote(), quote_results and quoted_slot_indices)
before scattering into all_responses, then perform the scatter and safely
collect the options (or return Err if any slot remains None). Ensure you update
the function's error return path to propagate this runtime check instead of
allowing unwrap()-based panics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b293940f-e9b0-41cc-a24c-277352fc82ab

📥 Commits

Reviewing files that changed from the base of the PR and between 6894f14 and c10ecf1.

📒 Files selected for processing (8)
  • crates/common/src/raindex_client/order_quotes.rs
  • crates/quote/ARCHITECTURE.md
  • crates/quote/src/cli/mod.rs
  • crates/quote/src/error.rs
  • crates/quote/src/order_quotes.rs
  • crates/quote/src/quote.rs
  • crates/quote/src/rpc.rs
  • packages/orderbook/test/js_api/raindexClient.test.ts

Comment thread crates/quote/ARCHITECTURE.md Outdated
Comment thread crates/quote/src/rpc.rs
@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from c10ecf1 to 160887d Compare April 15, 2026 17:13
@hardyjosh hardyjosh changed the base branch from 2026-04-14-take-orders-injected-signed-context to main April 15, 2026 17:13
@hardyjosh
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

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

@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from 160887d to 6df1e5c Compare April 15, 2026 17:15
hardyjosh pushed a commit to ST0x-Technology/st0x.rest.api that referenced this pull request Apr 15, 2026
Picks up the quote RPC refactor in rainlanguage/raindex#2550: quotes
now use orderbook-native `multicall(bytes[])` with
`from=counterparty`, preserving `msg.sender` through the batch. This
is what lets API-gated strategies (whose `calculate-io` asserts
`signed-context<0 0>() == order-counterparty()`) actually survive the
quote stage and enter the take-orders selection instead of being
silently filtered.

No code changes in this repo — the counterparty was already threaded
through to the rain.orderbook quote layer via the injector flow in
the parent PR; it just wasn't being used by the RPC layer until the
submodule fix.

Depends on rainlanguage/raindex#2550.
@hardyjosh hardyjosh requested review from JuaniRios and findolor April 15, 2026 20:15
@hardyjosh hardyjosh self-assigned this Apr 15, 2026
Comment thread crates/quote/src/order_quotes.rs Outdated
all_results.extend(results);
Ok(all_results)
// Scatter quote results back into the iteration-ordered response vector.
debug_assert_eq!(quote_results.len(), quoted_slot_indices.len());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be removed from the production code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in d868deb.

Comment thread crates/quote/src/rpc.rs
Comment on lines +129 to 155
Err(RpcError::ErrorResp(err_resp)) => {
if let Some(revert) = err_resp.as_revert_data() {
let decoded = match AbiDecodedErrorType::selector_registry_abi_decode(
revert.as_ref(),
registry,
)
.await
{
Ok(e) => results.push(Err(FailedQuote::RevertError(e))),
Err(e) => results.push(Err(FailedQuote::RevertErrorDecodeFailed(e))),
}
Ok(abi_err) => FailedQuote::RevertError(abi_err),
Err(err) => FailedQuote::RevertErrorDecodeFailed(err),
};
// Wrap the per-target failure as a chunk-level error so the
// bisection logic can decide whether to split or to attribute
// it to a single target.
Err(Error::ChunkReverted(Box::new(decoded)))
} else {
Err(Error::ChunkReverted(Box::new(
FailedQuote::CorruptReturnData(format!(
"RPC error without revert data: {err_resp}"
)),
)))
}
}
Err(e) => Err(Error::ChunkReverted(Box::new(
FailedQuote::CorruptReturnData(format!("eth_call transport error: {e}")),
))),
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Error handling is too broad: Logic in this block wraps all RPC errors (transport failures, deserialization errors, network issues) as Error::ChunkReverted(FailedQuote::CorruptReturnData(...)). This means a network timeout gets treated the same as a legitimate strategy revert and triggers bisection. You should only wrap ErrorResp with revert data as ChunkReverted; other RPC errors should propagate as infrastructure failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — d868deb adds a new Error::TransportError variant for non-ErrorResp RPC failures (transport, deser, etc). Only ErrorResp with revert data now produces ChunkReverted.

@hardyjosh hardyjosh force-pushed the 2026-04-15-quote-counterparty-msg-sender branch from 6df1e5c to d868deb Compare April 16, 2026 14:51
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
crates/quote/src/rpc.rs (1)

281-290: Consider using HashMap for O(n) grouping.

The current linear scan for grouping is O(n × g) where g is the number of distinct orderbooks. While this is likely acceptable for typical workloads, a HashMap<Address, Vec<...>> would be O(n).

♻️ Optional refactor using HashMap
+use std::collections::HashMap;
+
-    let mut groups: Vec<(Address, Vec<(usize, QuoteTarget)>)> = Vec::new();
-    for (i, target) in quote_targets.iter().enumerate() {
-        if let Some(group) = groups.iter_mut().find(|(ob, _)| *ob == target.orderbook) {
-            group.1.push((i, target.clone()));
-        } else {
-            groups.push((target.orderbook, vec![(i, target.clone())]));
-        }
-    }
+    let mut groups: HashMap<Address, Vec<(usize, QuoteTarget)>> = HashMap::new();
+    for (i, target) in quote_targets.iter().enumerate() {
+        groups
+            .entry(target.orderbook)
+            .or_default()
+            .push((i, target.clone()));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/quote/src/rpc.rs` around lines 281 - 290, The current grouping loop
(building `groups: Vec<(Address, Vec<(usize, QuoteTarget)>)>`) does an O(n×g)
scan; replace it with a HashMap-based O(n) grouping by using a HashMap<Address,
Vec<(usize, QuoteTarget)>>, insert or push into the map for each (i, target)
from `quote_targets.iter().enumerate()`, then convert the map into the final
`Vec<(Address, Vec<(usize, QuoteTarget)>)>` if you still need a Vec; update
references to `groups` accordingly and keep the original index and `QuoteTarget`
clones when inserting so scattering back into the result works the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/quote/src/rpc.rs`:
- Around line 281-290: The current grouping loop (building `groups:
Vec<(Address, Vec<(usize, QuoteTarget)>)>`) does an O(n×g) scan; replace it with
a HashMap-based O(n) grouping by using a HashMap<Address, Vec<(usize,
QuoteTarget)>>, insert or push into the map for each (i, target) from
`quote_targets.iter().enumerate()`, then convert the map into the final
`Vec<(Address, Vec<(usize, QuoteTarget)>)>` if you still need a Vec; update
references to `groups` accordingly and keep the original index and `QuoteTarget`
clones when inserting so scattering back into the result works the same.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b8830088-6a7f-4183-ab4f-5e028b48b84a

📥 Commits

Reviewing files that changed from the base of the PR and between c10ecf1 and d868deb.

📒 Files selected for processing (8)
  • crates/common/src/raindex_client/order_quotes.rs
  • crates/quote/ARCHITECTURE.md
  • crates/quote/src/cli/mod.rs
  • crates/quote/src/error.rs
  • crates/quote/src/order_quotes.rs
  • crates/quote/src/quote.rs
  • crates/quote/src/rpc.rs
  • packages/orderbook/test/js_api/raindexClient.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/orderbook/test/js_api/raindexClient.test.ts
  • crates/quote/ARCHITECTURE.md
  • crates/quote/src/cli/mod.rs

@hardyjosh hardyjosh requested a review from findolor April 16, 2026 15:40
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.

2 participants