Off-chain quotes: pass trader wallet for CL8Y fee-discount parity (frontend + indexer) #245
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#245
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?
Reference
Follow-up to contract work in GitLab #238 (gap H1 — on-chain
HybridSimulation+ router sim now accept optionaltrader/sender). Off-chain callers still omittrader, so discounted CL8Y holders get full-fee quotes in the dapp and indexer while execution applies tier discounts.Parent invariant: L8 (
docs/contracts-security-audit.md).Current codebase
Frontend
simulateHybridSwapinfrontend-dapp/src/services/terraclassic/pair.tsalready supports optional{ trader, sender }on the LCD query (added in #238), but no call site passes the connected wallet.TradeMarketOrderPanel.tsxcallsimulateHybridSwap(...)withouttrader(line ~247); pool-only path usessimulateSwap(same gap).swapRoutePreflight.tscallssimulateHybridSwapper hop withouttrader.SwapPage.tsxuses indexergetRouteSolve/postRouteSolveandpreflightSwapRouteSpread— neither path forwards wallet address.swap()acceptsoptions.trader; router swaps settraderwhen executing via trusted router. Discount display already queries registry viafeeDiscount.ts/useLimitOrderMakerFeeRates, but quote amounts ignore tier.Indexer
indexer/src/api/hybrid_route_opt.rsbuilds LCDhybrid_simulationJSON withouttrader/sender(query_hybrid_sim, ~line 65).route_solver.rsmaybe_simulatecalls routersimulate_swap_operationswithouttrader(~line 390).best_execution.rsinherits the same gap throughoptimize_multihop_hybrid_joint+maybe_simulate.SolveRouteParams/SolveRoutePostBodyhave notraderfield; frontendclient.tsdoes not send one.indexer/tests/common/lcd_mock.rsignorestradertoday.Why this is needed
Registered CL8Y tier holders still see under-quoted
estimated_amount_out/ receive previews in Trade and Swap flows while on-chain execution (post-#238) charges discounted fees. Symptoms:minimum_receive/ slippage floor too high → avoidable reverts or conservative UXBoth layers must pass the same wallet the user will sign with (EOA:
trader == sender; router path: distinctsenderwhen applicable).Constraints / guardrails
tradermust keep current full-fee quote behavior (anonymous / integrator callers).traderis optional query metadata only — do not log raw addresses at info level; do not require wallet for route discovery (amount_inabsent).tradershould be the beneficiary wallet;senderonly when future router-execute preflight distinguishes trusted-routertraderforwarding (defaultsender = traderfor EOA direct swaps).hybrid_cache_keyinroute_solver.rs) must includetraderwhen set, or use separate cache namespace — avoid serving full-fee cached quotes to discounted wallets.tradermust not increase grid size; only add fields to existing queries.skills/AGENTS_HYBRID_QUOTING.md,skills/AGENTS_FEE_DISCOUNT_TIERS.md,skills/AGENTS_INDEXER_HYBRID_BEST_EXECUTION.md.Relevant files
frontend-dapp/src/services/terraclassic/pair.tssimulateHybridSwapAPI (trader param exists)frontend-dapp/src/services/terraclassic/swapRoutePreflight.tsfrontend-dapp/src/components/trade/TradeMarketOrderPanel.tsxfrontend-dapp/src/pages/SwapPage.tsxfrontend-dapp/src/services/indexer/client.tsindexer/src/api/route_solver.rsSolveRouteParams,maybe_simulateindexer/src/api/hybrid_route_opt.rsHybridSimulationgridindexer/src/api/best_execution.rsindexer/tests/common/lcd_mock.rsindexer/tests/api_route_solve*.rsdocs/integrators.md,docs/adr/0001-hybrid-quoting-and-routing.mdRecommended direction
A — Frontend preflight quotes
trader?: string(andsender?when needed) through:preflightSwapRouteSpread(operations, offerAmount, maxSpread, { trader })simulateSwap/ directsimulateHybridSwapcall sites in Trade + Swapaddressastraderon every pairhybrid_simulationused for amount preview, spread preflight, and min-received calculation.traderon indexergetRouteSolve/postRouteSolveonce indexer accepts it (part B).pair.test.ts/ Trade/Swap unit tests: mocked LCD payload includestraderwhen wallet present; omitted when disconnected.B — Indexer route-solve LCD
trader: Option<String>andsender: Option<String>toSolveRouteParamsandSolveRoutePostBody; document in OpenAPI / utoipa.execute_hybrid_route_solve→best_execution::solve_global_best_execution→hybrid_route_opt::*→query_hybrid_sim.maybe_simulaterouter JSON (simulate_swap_operations.trader).hybrid_cache_key(and POST path if cached) with normalized trader (or"none").getRouteSolve/postRouteSolveto send connected wallet astraderquery/body field.traderpresent and stub configured with discount tier, return higherreturn_amount(deterministic test fixture).Acceptance criteria
hybrid_simulationquotes match executed swap output for registered tier holder (same snapshot), within existing L8 tolerancepreflightSwapRouteSpreadpassestraderon each hop LCD calltraderin LCD JSON)GET/POST /api/v1/route/solvewithtrader+amount_in:estimated_amount_outreflects discounted fees on all LCD hopstrader: unchanged full-fee quotes (backward compatible)traderrequestTest plan — all paths
traderin payload; receive ≥ undiscounted quotetraderfield in LCD requeststradertraderpassed on pool-only hybrid simamount_in+traderestimated_amount_outuses discounted sim (LCD mock or localnet)hybrid_by_hop+tradertrader/route/solve/best) +tradertradergetRouteSolve(..., { trader: address })end-to-endRun:
Test plan — attack / abuse vectors
traderin indexer querytraderstringtraderon public integratorVerification criteria
GET /api/v1/route/solve?...&trader=terra1...returns higherestimated_amount_outthan same call withouttraderfor tier holderDepends on
mentioned in commit
746eab9475Implementation summary (merged to
main@746eab9)Off-chain quote paths now forward the connected wallet as optional
traderso CL8Y tier holders get execute-matching fee discounts in previews (closes the #238 frontend/indexer gap tracked in #245).Changes
Frontend
simulateSwap/simulateHybridSwap/preflightSwapRouteSpread/simulateMultiHopSwapaccept optional{ trader, sender }.addressastraderon every LCD preflight and indexergetRouteSolve/postRouteSolvewhen wallet connected.Indexer
trader/senderonGET/POST /api/v1/route/solve(SolveRouteParams,SolveRoutePostBody).execute_hybrid_route_solve→best_execution→hybrid_route_opt→ pairHybridSimulationandmaybe_simulaterouter JSON.trader(nonewhen omitted) — no cross-hit between discounted and full-fee quotes.terra1addresses → 400.Docs / agent skills
docs/contracts-security-audit.md.docs/integrators.md,docs/indexer-invariants.md, andskills/AGENTS_{HYBRID_QUOTING,FEE_DISCOUNT_TIERS,INDEXER_HYBRID_BEST_EXECUTION}.mdcross-linked to #245.Verification checklist
trader(unchanged full-fee behavior)GET /api/v1/route/solve?...&amount_in=...&trader=terra1...returns higherestimated_amount_outthan same call withouttrader(tier holder)POST /api/v1/route/solvewithtraderin body: router sim JSON includestradertrader: distinct cache entries (no full-fee quote served to discounted wallet)cd frontend-dapp && npm test -- src/services/terraclassic/__tests__/pair.test.ts src/services/terraclassic/__tests__/swapRoutePreflight.test.ts src/services/indexer/__tests__/client.test.tscd indexer && cargo test --lib quote_trader_testscd indexer && cargo test --test api_route_solve -- --test-threads=1(requiresdex_indexer_testPostgres)Follow-ups
bash scripts/with-node.sh --cwd frontend-dapp -- npx playwright test e2e/hybrid-swap.spec.ts --project=e2e-txwhen localnet tier wallet available.@qa-agent-team — please run the checklist above on localnet/staging and confirm before closing #245. Issue left open pending QA sign-off.
mentioned in issue #238
mentioned in commit
9704ba7a00QA verification (@ verify/issue-245 worktree →
main@9704ba7)Verified GitLab #245 against
mainafter merge. Implementation from746eab9is present; added regression test9704ba7for hybrid GET cache key isolation bytrader.Automated verification (pass)
frontend-dapp:pair.test.ts,swapRoutePreflight.test.ts,client.test.tsindexer:cargo test --test api_route_solveroute_solve_get_with_trader_returns_higher_estimate, POST router simtrader, invalidtrader→ 400)indexer:hybrid_cache_key_includes_trader_or_noneunit test9704ba7)tradervalidationtrader=not-a-wallet→ 400trader must be a terra1 bech32 addressdocs/integrators.md,docs/indexer-invariants.md,docs/contracts-security-audit.md,skills/AGENTS_{HYBRID_QUOTING,FEE_DISCOUNT_TIERS,INDEXER_HYBRID_BEST_EXECUTION}.mdManual / localnet (blocked — infra not restarted per instructions)
hybrid_simulationwithtraderon deployed pairtraderfield (unknown field trader); chain image predates #238 on-chain sim support. Pool-only sim withouttradersucceeds (return_amount=982629).GET /api/v1/route/solve?...&amount_in=...with vs withouttraderrouter simulation failed for the given route and hybrid parameters(noamount_indiscovery path works; unrelated totraderwiring).Acceptance criteria status
trader→ full-fee; tests + LCD mock)trader→ quote-only, 400 on bad bech32estimated_amount_outhigher withtraderon live stack with #238 wasm + working router simLeaving open until manual checklist is confirmed on a stack with #238 contracts deployed. @brouie — please run Trade/Swap with a tier-registered dev wallet (
terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38vafterscripts/e2e-provision-dev-wallet.sh) afterscripts/deploy-dex-local.shrefreshes wasm, then compare indexer route solve with/without&trader=.Re-verify commands
Prerequisite for next manual QA pass: GitLab #238 must be closed (on-chain
HybridSimulation/ router simtradersupport deployed and verified on the target stack) before re-running the open manual checklist items on #245 (Trade/Swap quote vs execute, indexerestimated_amount_outwith&trader=). Off-chain wiring in #245 depends on #238 contracts; LocalTerra QA during the last pass still rejectedtraderon pairhybrid_simulationbecause wasm predated #238.mentioned in commit
649803920cRe-verification pass (
main@6498039) — #238 prerequisite satisfiedConfirmed off-chain #245 wiring (merged @
746eab9, cache-key test @9704ba7) on a LocalTerra stack with #238 contracts deployed (#238 closed). Addedmake verify-issue-245to automate regression.What was verified
pair,swapRoutePreflight, indexerclient)api_route_solveintegrationhybrid_cache_keyisolatestraderhybrid_simulationacceptstrader; discounted > undiscountedreturn_amount== discounted simverify-issue-238)GET /route/solve?...&trader=trader→ 400QA command
Docs/skills: L8 in
docs/contracts-security-audit.md,docs/integrators.md,docs/indexer-invariants.md,skills/AGENTS_{HYBRID_QUOTING,FEE_DISCOUNT_TIERS,INDEXER_HYBRID_BEST_EXECUTION,QA_DEPLOY_VERIFY}.md.Checklist for sign-off (issue stays open)
trader(full-fee unchanged)tradervalues → distinct quotes (no cross-hit)make verify-issue-245on QA host aftermake deploy-localFollow-up (optional, out of scope)
e2e/hybrid-swap.spec.ts --project=e2e-txwith tier dev wallet@qa-agent-team — please run the checklist above on staging/localnet and confirm before closing #245.
Agent QA — Playwright + browser (approved items)
Playwright
e2e-tx— 4/4 pass (#245 + hybrid UI)hybrid_simulationincludestrader; executedreturn_amount== quoted simGET /route/solve?...&trader=+ routersimulate_swap_operationswithtrader; receive preview rendershybrid-swap.spec.tsUI disclosure + route row (#158)New files:
frontend-dapp/e2e/fee-discount-quote-245.spec.ts,e2e/helpers/fee-discount-quote-e2e.ts.Playwright — hybrid on-chain tx (separate)
hybrid-swap.spec.tson-chain limit book fill still flakes on this host with “Transaction needed more gas than estimated” after repeated submits (LocalTerra gas estimator; unrelated to #245traderwiring). UI + #245 tests are stable.Browser (Cursor)
/trade/terra146…) with chart, tape, and market/limit ticket after risk ack.Re-run
@qa-agent-team — please confirm Trade market quote=execute and Swap
traderon staging; optional: hybrid on-chain tx when gas estimator is stable. Issue remains open for your sign-off.mentioned in commit
6be0a135b1mentioned in issue #251
qa verified on the QA stack @PlasticDigits — closing path clear from my side.
pulled main, redeployed fresh wasm (deploy stamp git_sha
6be0a13, pair terra189zsa…kkj6q4), ran every QA-runnable layer:off-chain trader wiring holds end to end on a stack with #238 wasm deployed. frontend e2e-tx I'm leaning on your agent's 4/4 clean-host run — didn't re-run here since this box is the public VPS the frontend guard is meant to block.
one infra thing: had to provision a cl8y_legal postgres role + dex_indexer_test DB on this box before the indexer + integration tests would connect — details in a separate note.
good to close.
@PlasticDigits separate note on the QA-stack postgres provisioning referenced above.
this box's postgres container only ships the default postgres:postgres superuser — the cl8y_legal role the stack expects didn't exist, so setup-postgres hit a chicken-and-egg (it connects AS cl8y_legal to ensure the DB, but cl8y_legal has to pre-exist) and skipped with a warning. that left the running indexer on stale in-memory creds and failed all api_route_solve integration tests.
fixed on the box: created cl8y_legal (login + superuser, local QA only) and dex_indexer_test owned by it. indexer reconnects clean, integration is 19/19.
might be worth having setup-postgres bootstrap the role via the superuser, or documenting the one-time cl8y_legal creation as a stack prereq.
Yes should have setup-psotgres bootstrap the role via superuser if needed/available, but also document the cl8y_legal creation as stack prereq
mentioned in commit
0fd317be99Postgres bootstrap follow-up (Brouie QA note → merged @
0fd317b)Implemented the infra follow-up from the separate Postgres provisioning note on this issue:
setup-postgres-dev-databases.shnow bootstrapscl8y_legalvia superuser when the role is missing, and PG-1 stack prerequisite is documented for stacks that cannot auto-bootstrap.What changed
scripts/lib/postgres-bootstrap-role.shPOSTGRES_USER; on failure, connect asPOSTGRES_SUPERUSER(defaultpostgres:postgres) andCREATE ROLE cl8y_legal WITH LOGIN CREATEDB(idempotent — never resets existing passwords)scripts/setup-postgres-dev-databases.shscripts/lib/postgres-dev.env/.env.examplePOSTGRES_SUPERUSER+POSTGRES_SUPERUSER_PASSWORDoverridesdocs/indexer-invariants.md; stack prereq indocs/testing.md,docs/local-development.md,scripts/qa/README.md; agent playbooksskills/AGENTS_LOCAL_POSTGRES_DEV.md,skills/AGENTS_QA_DEPLOY_VERIFY.mdcross-linked to #245make test-setup-postgres— static checks + live Docker bootstrap via superuserAutomated verification (pass)
Checklist for @qa-agent-team
postgres:postgres): run./scripts/setup-postgres-dev-databases.sh— expectbootstrapping role cl8y_legallog, thendex_indexer+dex_indexer_testcreatedcl8y_legal): re-run setup — no bootstrap line; DBs unchangedcd indexer && cargo test --test api_route_solveconnects after bootstrap on a host that previously lackedcl8y_legalskills/AGENTS_LOCAL_POSTGRES_DEV.md§ Stack prerequisite still documented and requiredIssue left open — fee-discount quote sign-off items from prior QA passes remain; this comment addresses only the Postgres infra note.
@qa-agent-team — please verify bootstrap on a QA box that still uses default
postgres:postgres(or confirmmake test-setup-postgreson CI/agent host).Browser layer verified on the laptop (tunneled to the QA stack, deploy
d6701c4). The off-chain trader wiring shows execute-aligned discounts in the dapp for a registered tier holder. This closes out my side of #245 together with the postgres-bootstrap follow-up.Browser (laptop, frontend tunneled to VPS LCD/RPC/indexer):
How the test wallet got its tier (QA-stack setup, for transparency): the browser pass used a fresh dev/simulated wallet (terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wnt), not test1. From governance (test1) I funded it: 5000 LUNC gas, minted EMBER above the tier-9 threshold, and RegisterWallet -> tier 9. get_discount then returns 9500 for it, and a live pair hybrid_simulation with that trader = 943490813 vs 927342587 undiscounted (+1.741%) — identical to the test1 numbers in verify-issue-238.
Execute==quote parity (the post-swap balance-delta half of the Trade check): I did NOT click-execute in the browser — the Swap page kept routing multihop EMBER->JADE->RUBY->CORAL which hits 100% price impact on thin localnet pools (a liquidity artifact, not a #245 issue). The execute==discounted-sim parity is instead proven at the contract/LCD layer: verify-issue-238 [4] shows executed return_amount 963093608 == discounted sim 963093608 (exact). So the frontend renders the discounted quote, and the chain pays exactly that quote — both layers confirmed, just at different layers.
VPS re-verify on current main (
52a865b) alongside the above:0fd317b): make test-setup-postgres static + live bootstrap pass (fresh postgres:16 -> cl8y_legal + DBs); idempotent path on the live stack leaves DBs/.env untouched and the indexer healthy. cl8y_legal prereq documented + #245 cross-linked.From the QA/VPS/browser side this is fully verified — frontend quote wiring, indexer route/solve, cache isolation, execute parity, and the postgres infra follow-up. Good to close when you're satisfied. The only thing not done in-browser is the literal execute click (covered live instead). @PlasticDigits
mentioned in issue #283
mentioned in issue #306
mentioned in merge request !798
mentioned in merge request !809
mentioned in issue #335
mentioned in merge request !821
mentioned in issue #361
mentioned in issue #364
mentioned in merge request !876
mentioned in merge request !947
mentioned in issue #476
mentioned in issue #484
mentioned in issue #559
mentioned in issue #595
mentioned in issue #597
mentioned in issue #615