Skip to content

feat(solver): external liquidity routing — RFQ websocket to other DEXes#7

Open
VAIBHAVJINDAL3012 wants to merge 6 commits into
vaibhav/filler-sdkfrom
vaibhav/external-liquidity-routing
Open

feat(solver): external liquidity routing — RFQ websocket to other DEXes#7
VAIBHAVJINDAL3012 wants to merge 6 commits into
vaibhav/filler-sdkfrom
vaibhav/external-liquidity-routing

Conversation

@VAIBHAVJINDAL3012

Copy link
Copy Markdown
Contributor

Stacked on #6 (vaibhav/filler-sdk). Review/merge that first — this PR is based on
vaibhav/filler-sdk so its diff shows only the routing work; GitHub will retarget the
base to main automatically once #6 merges.

What

Routes orders the matcher can't cross internally to allow-listed external DEXes over
a websocket RFQ: DEXes post standing {price, quantity} quotes, the matcher matches its
idle notes against them and hands over the serialized note bytes for permissionless
on-chain self-consume
. Off by default (router_enabled).

Design

Full reasoning in docs/adr/0001-external-liquidity-routing.md; as-built reference in
docs/external-liquidity-routing.md. Highlights:

  • Central in-memory order book as the hub — no DB poll, no second copy.
  • Park / unpark for in-flight notes — reuses the existing rate-index remove + counter
    path, so best_order / has_orders / active_pair_count need no change; a parked
    note is invisible to matching by construction. O(expiring) TTL reactivation via a
    time-ordered park_queue.
  • Decimals-correct export math vs oracle MID (select_notes) — exact u128,
    marginal-first ordering, off-market guard. fill_price is carried for the forthcoming
    overfill protocol (today the chain still settles at the note's rate).
  • Router = transport only on its own OS thread (mirrors spawn_price_api_thread);
    two Send channels to the matcher (watch quotes in, mpsc handovers out via
    try_send, so a slow DEX socket never stalls the fund-critical tick).
  • Allow-list auth via SOLVER_ROUTER_TOKENS (env), constant-time check at the
    websocket upgrade.

Testing

  • select_notes / order book / matcher / router: ≥95% line, 100% function coverage
    on changed files; property test asserts active_order_count() invariance across
    park→unpark and park→consume.
  • Seamless e2e (crates/solver/tests/integration_filler_sdk.rs) drives the real
    router thread through the public FillerClient: bad token rejected → AuthOk → quote
    reaches the matcher → handover surfaces as a FillerEvent with the exact bytes +
    fill_price.

Risks (documented in the ADR)

  • Flow leak (accepted): a trusted DEX sees a byte-level view of the unmatched
    residual; partial mitigation (no re-offer to the same DEX on reactivation) shipped,
    per-DEX cap deferred.
  • No custody risk: a handover is bytes; worst case is wasted in-flight windows bounded
    by the TTL.

Docs / ADR

  • docs/external-liquidity-routing.md, docs/adr/0001-external-liquidity-routing.md.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Important

Review skipped

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

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac9217b3-a729-4273-b5d0-2d215759de43

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vaibhav/external-liquidity-routing

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.

@VAIBHAVJINDAL3012 VAIBHAVJINDAL3012 force-pushed the vaibhav/external-liquidity-routing branch from af69d8b to f76ebec Compare June 25, 2026 19:24
Route orders the matcher can't cross internally to allow-listed external
DEXes over a websocket RFQ: DEXes post standing {price, quantity} quotes,
the matcher matches idle notes against them and hands over the serialized
note bytes for permissionless on-chain self-consume. Off by default
(router_enabled).

- order book: park/unpark for in-flight notes (reuses the index path, so the
  matching gates need no change), O(expiring) TTL reactivation
- router/select: decimals-correct export math vs oracle mid, marginal-first,
  off-market guard; fill_price carried for the forthcoming overfill protocol
- router/server: websocket transport on its own OS thread, allow-list auth
- matcher: external pass + reactivation each tick (try_send handovers)
- config/pipeline/start wiring; decimals snapshot; load_token_decimals

Builds on the pswap-filler-sdk crate (shared wire protocol). See
docs/adr/0001-external-liquidity-routing.md and docs/external-liquidity-routing.md.
A DEX (FillerClient) connects to the real router thread and posts an RFQ
quote; an unmatched order in run_matcher's external pass is selected and
handed back to the SDK as a FillerEvent::Handover — SDK quote → router →
matcher select → router → SDK, nothing mocked in between. Asserts note id,
fill amount, fill_price, and exact note bytes survive the round trip.
…verage

- matcher: a full handover channel drops the handover but still parks the
  note (TTL recovers it) and never blocks the tick — covers the try_send
  Full branch.
- router: a message over max_msg_bytes closes only that connection; the
  router keeps serving a fresh client (DoS guard).
- SDK ↔ router: an unparseable quote comes back to the SDK as a structured
  FillerEvent::Error and the session stays open.
Mirror the SDK-side removal of the never-emitted Withdrawn event in the
external-liquidity-routing message list, and re-sync Cargo.lock with this
branch's solver ws/futures-util deps (the rebase carried the SDK branch's
pruned lock).
@VAIBHAVJINDAL3012 VAIBHAVJINDAL3012 force-pushed the vaibhav/external-liquidity-routing branch from f76ebec to a8dd8fc Compare June 26, 2026 05:10
When the handover channel is full, try_send drops the batch — but the notes
were already parked, so TTL reactivation would later add a no-re-offer block
for a DEX that never received them (the matcher couldn't tell "dropped" from
"delivered-but-not-consumed"). Now a dropped batch is rolled back: each note
is unparked and its reservation released, so it stays immediately eligible to
the same DEX next tick.

Adds OrderBook::unpark — a no-penalty single-note rollback that re-indexes the
note and leaves a harmless park_queue tombstone. Tests: a direct unpark unit
(rollback + tombstone-skip + no-op-when-unparked); the backpressure test now
asserts the rollback and a same-DEX re-route on retry.
…xit)

Reflect the fix: a dropped handover try_send now immediately unparks the note
with no re-offer penalty, so the no-show penalty applies only to genuinely
delivered handovers.
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