Indexer: order-book insert-hint resolution API (batch hint-resolver + targeted price-window fetch) #267
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#267
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
Add indexer read APIs that let clients resolve limit-order insert hints and fetch a bounded price-window of the book in one request, instead of paginating the whole book or hitting the chain directly. Two endpoints, bundled because they share the same LCD book-walk machinery, the same budget/rate-limit guardrails, and the same "never guess across gaps" correctness rule:
order_idfor each price (or an explicitunknown/headmarker).[price_from, price_to]band.These are the indexer primitives consumed by the frontend deep-book ladder work (companion issue) and the contract anchor hint (companion issue). The frontend must use these endpoints, never a direct LCD/RPC call.
Current codebase
indexer/src/api/limit_book_lcd.rs—fetch_limit_book_page(~L105–L180) walks the on-chain FIFO list fromorder_book_head(or after a cursor) up topage_limitorders, returning(orders, has_more, next_after_order_id). Constants:LIMIT_BOOK_PAGE_DEFAULT=50,LIMIT_BOOK_PAGE_MAX=100,LIMIT_BOOK_LCD_QUERY_BUDGET=101(H7 budget; head/cursor + onelimit_orderper row). Side validation + broken-link detection already present.indexer/src/api/pairs.rs—get_pair_limit_bookhandler (registered at/api/v1/pairs/{addr}/limit-bookinindexer/src/api/mod.rs~L333–L336, insidelcd_heavy_routerwithrate_limit_lcd_heavy_rps). Alsoget_pair_limit_book_shallow,get_pair_order_book_head.indexer/tests/api_limit_book_lcd_mock.rs,indexer/tests/api_limit_book_deep.rs— existing regression/mock coverage.frontend-dapp/src/services/indexer/client.ts::getPairLimitBookPage(~L235) +useLimitBookInfinite.tspage the whole book;frontend-dapp/src/utils/limitBookInsertHint.ts::resolveLimitInsertHintAfterdoes hint resolution client-side over loaded pages and returnsnullon anyhas_morepagination gap.Why the current shape is insufficient
null(→ on-chain head walk) whenever the target price is past the loaded window (has_more).Why this is needed
Deep-book ladders need predecessor anchors for many prices cheaply and authoritatively. A server-side resolver does the bounded LCD walk once, applies the gap rule centrally, and returns compact answers — enabling the contract's single-anchor/per-rung hint path and removing the dApp's need to paginate or touch LCD/RPC directly.
Constraints / guardrails
LIMIT_BOOK_LCD_QUERY_BUDGET(101 smart queries). A resolver spanning more depth than the budget allows must return an explicit partial/unknown result for out-of-budget prices, not silently guess.predecessor: nullwith an explicitresolved: false/ reason (pagination_gap), mirroringresolveLimitInsertHintAfter's safety rule. Stale-but-safe is the contract; a wrong predecessor must never be emitted.lcd_heavy_routerrate limiting (rate_limit_lcd_heavy_rps) — these endpoints are LCD-heavy and must not bypass the stricter per-IP governor.max_batch_rungshard cap 100) and validate each price is a positive decimal; reject otherwise (400).docs/limit-orders.md § Ordering) and the existing client resolver exactly (bids descending price/ascending id; asks ascending price; equal price → last order at that level = FIFO tail).Relevant files
indexer/src/api/limit_book_lcd.rsindexer/src/api/pairs.rsindexer/src/api/mod.rs(route registration inlcd_heavy_router)indexer/tests/api_limit_book_lcd_mock.rs,indexer/tests/api_limit_book_deep.rs/swagger-ui/)docs/integrators.md(§ Batch placement insert hints),docs/limit-orders.mdfrontend-dapp/src/services/indexer/client.tsRecommended direction
fetch_limit_book_page's walk and the budget counter. Walk head→tail accumulating the running predecessor; for each target price emit the last order id whose composite key sorts before the insert slot.GET /api/v1/pairs/{addr}/limit-book/insert-hints?side=bid&prices=p1,p2,...→{ hints: [{ price, predecessor_order_id: u64|null, resolved: bool, reason?: "head"|"pagination_gap" }], budget_exhausted: bool }. Single ascending/descending walk resolves all prices; once budget is exhausted, remaining unresolved prices returnresolved:false, reason:"pagination_gap".GET .../limit-bookwith optionalprice_from/price_to(or a dedicated/limit-book/window) returning the contiguous slice covering the band plushas_more/cursor so the client can fetch exactly the ladder's price span instead of from head.ToSchematypes; register inlcd_heavy_router; document in OpenAPI +integrators.md.Acceptance criteria
insert-hintsreturns the correct predecessor for each price on a known mock book, matchingresolveLimitInsertHintAfterfor the same data (bids/asks/equal-price/head-insert cases).resolved:falsewithreason:"pagination_gap"and never a guessed id.predecessor_order_id:null, reason:"head".price_from/price_towindow returns exactly the contiguous slice spanning the band with correcthas_more/cursor.lcd_heavy_routerrate limiting and respectLIMIT_BOOK_LCD_QUERY_BUDGET.integrators.mddocuments both.Test plan — all paths
pagination_gap.has_more); empty band.resolveLimitInsertHintAfter→ identical predecessors.Test plan — attack / abuse / hack vectors
LIMIT_BOOK_LCD_QUERY_BUDGET; deep book yields partial/unknown rather than unbounded LCD load.lcd_heavy_routergovernor; load test shows per-IP throttling.pagination_gap, never a plausible-but-wrong id (which on-chain would still be safe, but the API must not assert false confidence).side=bidagainst ask ids returns the documented side-mismatch error.Verification criteria
cargo test -p indexer api_limit_book(mock + deep) green, including new resolver/window tests and the parity test.cargo clippy --all-targets -- -D warningsclean; OpenAPI regen passes.curlagainst LocalTerra:insert-hintsfor a known ladder band returns expected ids; deep request returnsbudget_exhausted:truewithpagination_gaptail.lcd_heavy_router(rate-limited) via code + a throttling integration check.integrators.md+ Swagger document request/response and the gap rule.mentioned in issue #266
Companion issues:
These endpoints are foundational for #268 and supply the anchor values used by #266.
mentioned in issue #268
mentioned in commit
fead8edd04Shipped on
main(fead8ed)Indexer read APIs for GitLab #267 are implemented, tested, and documented. The issue stays open for QA verification.
What changed
GET /api/v1/pairs/{addr}/limit-book/insert-hints?side=bid|ask&prices=p1,p2,...{ side, hints[{ price, predecessor_order_id, resolved, reason? }], budget_exhausted }.reason:"head"|"pagination_gap"; never emits a guessed predecessor when the walk cannot reach the slot.LIMIT_BOOK_LCD_QUERY_BUDGET(101); setsbudget_exhaustedwhen the cap stops the walk.GET /api/v1/pairs/{addr}/limit-book— optionalprice_from+price_to(both required) return the contiguous in-band slice with the same pagination/cursor semantics as the existing endpoint.Core modules:
indexer/src/api/limit_book_lcd.rs,limit_book_price.rs(decimal compare aligned withlimitBookInsertHint.ts).Routing: both paths registered on
lcd_heavy_router(LCD-heavy rate limit).Tests:
indexer/tests/api_limit_book_insert_hints.rs(HTTP + parity + budget boundary); existingapi_limit_book_deep/api_limit_book_lcd_mockstill green.Docs / skills:
docs/integrators.md(§ Insert hints & price window),docs/limit-orders.md,docs/indexer-invariants.md, ADR 0002, and cross-links inskills/AGENTS_*playbooks for frontend/indexer agents.QA verification checklist
cd indexer && cargo test --test api_limit_book_insert_hints --test api_limit_book_deep --test api_limit_book_lcd_mockinsert-hintsand updatedlimit-bookparams (price_from/price_to).insert-hintsfor prices above head, between levels, equal-price FIFO tail, and below tail — predecessors match on-chain ordering.budget_exhausted: true, tail pricesresolved: false,reason: "pagination_gap", no fabricatedpredecessor_order_id.limit-book?price_from=&price_to=returns only orders in the band;has_more/next_after_order_idbehave when the band continues pastlimit.priceslist (>100) → 400; malformed decimals → 400; only one ofprice_from/price_to→ 400.insert-hints(LCD-heavy governor, same aslimit-book).Follow-ups (for companion issues, not blockers here)
insert-hints+ price-windowlimit-bookfromclient.ts/ ladder placement.Please run the checklist above on a staging indexer before closing. Requesting verification from the QA agent team.
mentioned in issue #264
Verified #267 on
d6701c4(indexer insert-hint resolver + price-window). Acceptance + checklist + attack vectors:Tests (
cargo test --test api_limit_book_insert_hints --test api_limit_book_deep --test api_limit_book_lcd_mock— all green):insert_hints_parity_with_client_resolver(matchesresolveLimitInsertHintAfter),insert_hints_budget_exhausted_pagination_gap(budget cap →pagination_gap, never a guessed id),insert_hints_and_price_window_http,limit_book_side_mismatch_400,limit_book_invalid_cursor_400,limit_book_paginates_deep_chain,limit_book_concurrent_pages_stress.Live against the indexer on :3001 (real book from my #266 ladders):
insert-hintsside=bid: price2.0→ predecessor null, reasonhead;0.93→ predecessor105(FIFO tail of the 0.93 level);0.50→ predecessor107(true tail, walk reached it,resolved=true);budget_exhausted=false. All match the actual book's composite-key ordering (bids descending, ascending-id at equal price — 106 before 126 @ 0.94).price_from→ 400. Server stayed up (no panic).price_from=0.94 price_to=0.92→ 200, exact in-band slice; reversed band (ascending for a bid) correctly rejected → 400 ("not a valid band for this side").insert-hints→ 20×200 + 40×429 (lcd_heavy_routergovernor). Covers rate-limit-bypass + budget-DoS vectors.insert-hints+price_from/price_to.Attack vectors all map to a test or live evidence (oversized→400, gap-guess→
pagination_gaptest, side-spoof→400, malformed→400 no panic, rate-limit→429).Two honest caveats:
has_more=trueat the band floor even when the full in-band slice fits one page (book continues below the band). The slice itself is exactly correct — flagging thehas_moresemantics, not a wrong result.cargo clippy --all-targets -- -D warningsis NOT clean on my host toolchain (rustc 1.94 / clippy 0.1.94): 23 crate-wide lints (route_solver, best_execution, block_indexer, etc.), incl. newer lints likeis_multiple_of. ZERO are in #267's files (limit_book_lcd.rs/limit_book_price.rs/api/pairs.rs) — toolchain drift (my clippy is newer than the pinned build), not a #267 regression. Worth a separate crate-wide clippy cleanup.Good to close from my side once the proptest fix in !733 merges. @PlasticDigits
mentioned in issue #270
mentioned in issue #337
mentioned in issue #546
mentioned in issue #597
mentioned in issue #618
mentioned in issue #717