feat(indexer): unified global best-execution route solver (multi-path + joint hybrid optimization) #209
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#209
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Replace the indexer’s fragmented route solver (BFS hop-count path + sequential per-hop hybrid grid + optional client
hybrid_by_hopon POST) with a unified “best execution” API that returns a globally optimal (within documented bounds) multihop route: path selection, per-hop pool/book splits, and router operations chosen to maximizeestimated_amount_outunder on-chain simulation constraints.Follow-up to closed epics #101 / #108 and gap analysis
gaps/GAP_1780023683.md(route solver row).Current codebase
Endpoints (
indexer/src/api/mod.rs)GET/api/v1/route/solveamount_in: route discovery only — BFS over indexed pairs,router_operationswithhybrid: null. Withamount_in(default): hybrid path — BFS (max 3 hops), thenhybrid_route_opt::optimize_multihop_hybrid, LCDsimulate_swap_operationswhenROUTER_ADDRESSset.GET/api/v1/route/solve/bestamount_inrequired (#189).POST/api/v1/route/solvehybrid_by_hopmerged per hop; LCD sim when configured. Client must supply splits for non-default hybrid.Path discovery (
route_solver.rs)find_path: unweighted BFS on the pair graph; returns the first shortest path by hop count (not best output).assets.contract_address(CW20); native-only assets are not routable.Hybrid optimization (
hybrid_route_opt.rs)optimize_multihop_hybrid: sequential per-hop grid search (GRID_POINTS = 17) overbook_input; output of hop i feeds hop i+1.quote_kind: indexer_hybrid_lcd_degraded).hybrid_notes— "Sequential per-hop hybrid optimizer (not globally optimal across hops)…".Caching & limits
1_000_000for hybrid GET keys.MAX_HOPS = 4; indexer GET hybrid caps at 3 hops (#191).Consumers
frontend-dapp/src/services/indexer/client.ts(getRouteSolve,postRouteSolve);SwapPage.tsxuses indexer routes and can POSThybrid_by_hopaligned to UI splits.docs/integrators.md,docs/indexer-invariants.md, ADRdocs/adr/0001-hybrid-quoting-and-routing.md.skills/AGENTS_INDEXER_HYBRID_BEST_EXECUTION.md.Tests today
indexer/tests/api_route_solve.rs: discovery, POST merge, hybrid length mismatch, GET hybrid 2/3-hop with LCD mock — no multi-path or global optimality assertions.Why this is needed
amount_in, not “shortest BFS path + greedy per-hop splits.” Today a longer path or different split schedule can beat the indexer answer;hybrid_notesadmits this but does not fix it./best, POST +hybrid_by_hop) force clients to understand implementation details instead of one “solve best route” contract.gaps/GAP_1780023683.mdandARCHITECTURE_GAP_MATRIX.mdlist “Best execution logic — sequential per-hop, not global.”estimated_amount_outcan be materially worse than achievable on the same chain snapshot, increasing slippage surprises at submit time.Constraints and guardrails
simulate_swap_operationsonROUTER_ADDRESS(same as today). Pair-level candidates useHybridSimulationonly (ADR 0001 / #190).MAX_HOPS(4). Document policy for retail GET (3 vs 4) after global solver ships; do not exceed chain limits.quote_kind/hybrid_notes(never silent best-guess).indexer/tests/security.rsand production rate limits; avoid unbounded fan-out per request.max_spread/ min receive on execute.hybrid_by_hop-first POST workflows: keep POST for explicit overrides; default “best” should be GET (or single POST flagmode=best).contract_addressonly for graph nodes; unchanged unless separate native-routing issue lands.hybrid_notesto describe solver version, search bounds, and optimality claim (e.g. “optimal within top-K paths and split grid” vs “globally optimal”).Relevant files
indexer/src/api/route_solver.rsindexer/src/api/hybrid_route_opt.rsindexer/src/api/mod.rsindexer/src/lcd/indexer/src/config.rsROUTER_ADDRESSindexer/tests/api_route_solve.rssmartcontracts/contracts/router/src/contract.rsMAX_HOPS, simulate/executefrontend-dapp/src/services/indexer/client.tsfrontend-dapp/src/pages/SwapPage.tsxdocs/indexer-invariants.md,docs/integrators.mddocs/adr/0001-hybrid-quoting-and-routing.mdRecommended direction
Multi-path candidate generation
simulate_swap_operationsor per-path pool-only upper bound before expensive hybrid search.Global hybrid search (per candidate path)
amount_inconstraint, orsimulate_swap_operationson the winning ops.Unified API
GET /api/v1/route/solve?amount_in=…(and/bestalias) returns globally chosen path +hybrid_by_hop+router_operations.hybrid_by_hopfor integrator override only; optionalmode=discoveryvsmode=best.solver_version,paths_considered,optimality_scope, updatedhybrid_notes.Performance
ADR
Acceptance criteria
estimated_amount_outvs old BFS+sequential baseline (integration test with deterministic LCD mock).amount_inon hybrid best → 400.hybrid_notes/quote_kindaccurately reflect degraded and non-degraded outcomes.IndexerRouteSolveResponseupdated; integrator docs andindexer-invariants.mdupdated.pool_only=trueescape hatch still works (pool-only ops, 4-hop cap) without invoking global hybrid search.SwapPageuses unified best GET without manualhybrid_by_hopon happy path (can be follow-up issue if scoped).Test plan — functional paths
book_inputon winning hop;indexer_hybrid_lcdamount_inhybrid: null; no solver runpool_only=true+amount_inhybrid: null; pool LCD kindGET /solve/bestwithoutamount_inhybrid_by_hophybrid_by_hoplength ≠ hopsmax_maker_fillsboundary (1, large)Run:
cd indexer && cargo test api_route_solve -- --test-threads=1Test plan — attack vectors / abuse
amount_in+ many pairs → path explosionamount_in— negative, non-integer, overflow stringpool_input+book_input≠ hop offermax_maker_fillsMutexcacheoptimality_scopematches actual search bounds (contract test on JSON schema)Also run:
cargo test --test security(rate limits) after changing route handler cost.Verification criteria
api_route_solve.rsprove multi-path win over legacy BFS+sequential (feature flag or baseline comparison function in test only).cargo test --tests -j 1 -- --test-threads=1.smartcontracts/tests).docs/indexer-invariants.md,docs/integrators.md, and ADR updated; gap matrix row for “Best execution logic” → Done or Partial with explicit optimality scope.paths_considered,lcd_queries,solver_version,degradedon each best-exec request (structured fields).Out of scope (track separately)
Related issues
marked as related to #101
marked as related to #108
marked as related to #189
marked as related to #191
mentioned in commit
07ff8055c5mentioned in commit
f0ac4242e2mentioned in commit
bb00a76567Implementation summary (#209)
Merged to
main(07ff805): global best-execution route solver (solver_version:global_v1).What changed
route_paths::find_paths_top_k), not first BFS path only.book_inputgrids (optimize_multihop_hybrid_joint), then pick the candidate with highest routersimulate_swap_operationsestimated_amount_out./api/v1/route/solve(and/bestalias) use this engine whenamount_inis set;pool_only=trueunchanged (4-hop pool-only escape hatch).hybrid_by_hopoverrides.solver_version,paths_considered,optimality_scope,lcd_hybrid_queries, updatedhybrid_notes.Verification checklist
cd indexer && cargo test --test api_route_solve -- --test-threads=1(16 tests, incl. multi-path winner)GET /api/v1/route/solve?token_in=…&token_out=…&amount_in=…returnssolver_version: global_v1andpaths_considered≥ 1estimated_amount_out, not fewer hopspool_only=truestill returns 4-hop cap, allhybrid: nullGET /solve/bestwithoutamount_in→ 400hybrid_notes/optimality_scopematch bounded search (not unqualified “globally optimal”)@brouie — please verify on your QA stack when convenient. Leaving issue open until sign-off.
mentioned in commit
a648d25754Verification pass (agent, 2026-05-29)
Verified #209 on worktree
verify/issue-209againstmain(global solver already merged in07ff805). Pushed a small follow-up tomain(a648d25): integrationclean_dbuses TRUNCATE CASCADE (keeps existing flock lock); GAP executive summary aligned with global_v1 status.Automated checks (this environment)
cd indexer && cargo test --test api_route_solve -- --test-threads=1— 16/16 passed (incl.route_solve_global_picks_best_path_not_shortest, metadata contract)cd indexer && cargo test --tests -j 1 -- --test-threads=1— all integration tests passedcd indexer && cargo test --test security -- --test-threads=1— 19/19 passedcd smartcontracts && cargo test hybrid --lib— router/pair hybrid multihop tests passedbest_execution.rs(global_v1, top-5 paths, joint hybrid), ADR 0002,indexer-invariants.md,integrators.md,skills/AGENTS_INDEXER_HYBRID_BEST_EXECUTION.md, gap matrix row Partialtracing::info!on best-exec withsolver_version,paths_considered,lcd_queries,degradedSwapPageusesgetRouteSolve(..., amountIn)without POSThybrid_by_hopfor CW20↔CW20Not completed here (needs QA stack)
estimated_amount_outvs baseline on same blocksolver_version)Follow-up checklist for @brouie
GET /api/v1/route/solve?token_in=…&token_out=…&amount_in=…→solver_version: global_v1,paths_considered≥ 1,optimality_scopedescribes top-5 + grid boundsestimated_amount_out, not fewer hopspool_only=true+amount_in→ 4-hop cap, allhybrid: null,quote_kindpool LCDGET /solve/bestwithoutamount_in→ 400paths_considered/lcd_queries/degradedLeaving open until manual QA on a multi-route QA deploy is signed off.
Verification complete (infra restored)
All #209 verification criteria passed on live local stack:
Automated
cargo test --test api_route_solve— 16/16cargo test --tests -j 1 -- --test-threads=1— all integration tests greencargo test --test security— 19/19cargo test hybrid --lib(smartcontracts) — passLive manual QA
GET /route/solve?amount_in=…→solver_version: global_v1,paths_consideredup to 5, boundedoptimality_scopepaths_considered=5, 3 hops); many pairs showpaths_considered≥ 2pool_only=true— pool LCD, no global metadata pollutionGET /solve/bestwithoutamount_in→ 400tracing::info!("route best execution", solver_version, paths_considered, lcd_queries, degraded)inbest_execution.rsClosing as verified.
mentioned in issue #310
marked as related to #319
mentioned in issue #319
mentioned in issue #485
mentioned in issue #690
mentioned in issue #707
mentioned in issue #708