feat: Protocol DEX hub — cUSTC token contract link + LUNC/USD column #570
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#570
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
On
/protocol, the DEX hub prices card (protocol-dex-hub-prices) shows cUSTC / USD, UST1 / USD, and USTR / USD. UST1 and USTR render an explorer/copyAddressRowunder the price; cUSTC does not. Add the cUSTC wrap CW20 contract link in that column, and add a fourth column LUNC / USD (CEX LUNC mark, same 1:1 wrap pattern as cUSTC) with a cLUNC wrap CW20 contract link.Related: #556 (DEX hub USD + Protocol card), #550 (Protocol layout + CEX tabs), #515 (CEX
ustc|lunc|vfdusd), #541 (token identity / explorer), #378 (factory/router audit rows stay on/protocolonly), #489 (short copy).Current codebase
Protocol DEX hub card
ProtocolDexHubPrices.tsxmaps allowlisted tickers fromhubPriceTicker.ts:Layout is
sm:grid-cols-3. Each cell showsHUB_PRICE_TICKER_LABEL[ticker] / USD,formatHubUsd(row?.price_usd)(formatPairPrice, em-dash on null/non-finite/≤0), then only ifrow.source_pairis truthy anAddressRowof that address (copy + explorer viagetExplorerAddressUrl).GET /api/v1/hub-pricesreturns{ metadata, tickers, prices[] }withticker,price_usd,source_pair,tvl_usd,updated_at. It does not return the hub token CW20.Indexer semantics (#556 H1–H10,
AGENTS_INDEXER_HUB_USD.md):source_pairtodaycustcusd(cUSTC) = usd(uusd)). Oracle down → NULL, not$1.ust1ustrSo the AddressRows under UST1/USTR are source pair (pricing pool), not the UST1/USTR token contracts. The cUSTC column has no AddressRow because
source_pairis null. Operators looking at/protocolcannot click through to the cUSTC wrap they are actually quoting.Hub identity is already configured:
HUB_CUSTC_ADDRESS/HUB_UST1_ADDRESS/HUB_USTR_ADDRESS(indexer/.env.example,indexer/src/config.rsDEFAULT_HUB_CUSTC_ADDRESS).VITE_USTC_C_TOKEN_ADDRESS→USTC_C_TOKEN_ADDRESS;VITE_LUNC_C_TOKEN_ADDRESS→LUNC_C_TOKEN_ADDRESS(constants.ts). Mainnet cUSTC also appears asMAINNET_CUSTC_TOKEN_ADDRESSinust1SecondaryMarket.ts.hub_prices.tickerCHECK isIN ('custc', 'ust1', 'ustr')(indexer/migrations/20260818180000_hub_prices.sql).GET /api/v1/hub-prices/{ticker}unknown → 400.GET /api/v1/oracle/price/custc(andust1,ustr) stay 400 (H6). CEX catalog staysustc|lunc|vfdusd.LUNC today
LUNC/USD already exists as a CEX oracle tab on the same page (
ProtocolOracleCard.tsx,?ticker=luncallowlisted inprotocolOracleTicker.ts). Volume ingest already uses the #515 LUNC feed for LUNC / cLUNC /uluna(P522-Q / X4). There is no DEX-hub column, so the census card that shows cUSTC/UST1/USTR cannot show LUNC next to them without switching CEX tabs (and the CEX card is history + sources, not a compact four-mark strip).Native
ulunais not a CW20. Token identity (T541-2): do not invent a Finder URL for native denoms — copy-only.Page order (do not shuffle)
Title → Global stats → DEX hub prices → one CEX oracle card → factory/router audit → hooks (P550-1 / H5).
Why this is needed
source_pairfor cUSTC/LUNC (deepest UST1/cUSTC pool, etc.) would mislabel a CEX-oracle wrap as a pool mark. The missing chrome is token identity, not a fake pair.Constraints / guardrails
/ust1window, or settlement. Short copy (#489) — no essays.lunc(orclunc) as a fourth CEX oracle tab. CEX tabs remain exactlyustc|lunc|vfdusd(P550-2).?ticker=allowlist unchanged. Do not fetch CEXustr/ust1/custc.lunconGET /api/v1/oracle/priceas a new alias.GET /oracle/price/luncalready exists (CEX). Unknown CEX tickers stay 400. Hub DEX snapshot staysGET /api/v1/hub-prices(H6).—, not$1/$0. Do not rank a factory pair to populate cUSTCsource_pair.usd(cLUNC) = usd(uluna) = lunc_oracle), same wrap 1:1 as cUSTC. Oracle down →—. Do not take LUNC from a DEX pool (manipulable thin book). Do not use vFDUSD/FDUSD (P550-10 / H7).HUB_CUSTC_ADDRESS/VITE_USTC_C_TOKEN_ADDRESS). LUNC column link = configured cLUNC wrap CW20 (VITE_LUNC_C_TOKEN_ADDRESS/ newHUB_CLUNC_ADDRESS). Never matchsymbol === 'cUSTC'/'LUNC'. Never substring-matchcUSTCinsideUST1.ulunais copy-only (T541-2). Do not build a Finder/address/ulunaURL. Label the column LUNC / USD; the explorer target is the cLUNC wrap, with aria-labels that say wrap/cLUNC, not “native LUNC contract”.getExplorerAddressUrl(T541-2 / #430). Invalid bech32,javascript:,data:, HTML,../, empty, wrong HRP → omit the<a>, keep copy if the string is a valid allowlisted address.rel="noopener noreferrer"+target="_blank".AddressRowonto Swap confirmation (P550-12 / #378). This work is/protocolhub card only.volume_usdingest, Charts Price (USD),/ust1window, wrap fees, or limitvalidate_limit_order_price. Adding LUNC to the hub display must not retarget volume conversion (already LUNC CEX).source_pairrows. Those addresses are pairs, not tokens. Do not relabel them as “UST1 token” / “USTR token”. Optional additive tokenAddressRowfor UST1/USTR is in scope only if it stays visually secondary and aria-labels distinguish token vs source pair — default is: add token rows for oracle-anchored columns only (cUSTC, LUNC) to close the gap without doubling chrome (#489).parseHubPriceTicker/HubTicker::parse: homoglyphs,../,javascript:, extra underscores →null/ 400. Never interpolate raw path/query into SQL orfetchURLs.grid-cols-1 sm:grid-cols-2 xl:grid-cols-4(or equivalent), not a forcedsm:grid-cols-4that crushes AddressRows.detectMarketDataOutage. Prefer onegetHubPrices()so the existing hub query remains the only extra Protocol fetch.HUB_*/VITE_*_C_TOKEN_ADDRESSare set.Relevant files
frontend-dapp/src/components/protocol/ProtocolDexHubPrices.tsxfrontend-dapp/src/utils/hubPriceTicker.tsfrontend-dapp/src/utils/__tests__/hubPriceTicker.test.tsfrontend-dapp/src/pages/ProtocolPage.tsxfrontend-dapp/src/pages/ProtocolPage.test.tsxustr)frontend-dapp/e2e/protocol-page.spec.tsfrontend-dapp/src/components/protocol/useProtocolHubPricesQuery.tsGET /hub-pricesfrontend-dapp/src/components/ui/AddressRow.tsxfrontend-dapp/src/utils/terraExplorer.tsgetExplorerAddressUrlfrontend-dapp/src/utils/constants.tsUSTC_C_TOKEN_ADDRESS,LUNC_C_TOKEN_ADDRESSfrontend-dapp/src/types/index.tsIndexerHubPriceEntryindexer/src/api/hub_prices.rscustc|ust1|ustrindexer/src/indexer/hub_usd.rsindexer/src/db/queries/hub_prices.rsindexer/migrations/20260818180000_hub_prices.sqlindexer/tests/api_hub_prices.rsindexer/src/api/oracle.rsskills/AGENTS_INDEXER_HUB_USD.mdskills/AGENTS_FRONTEND_PROTOCOL_STATS.mddocs/frontend.mddocs/indexer-invariants.mdMakefile+scripts/qa/verify-issue-556.shverify-issue-<iid>Recommended direction
1. Token identity on oracle-anchored hub columns (cUSTC, LUNC)
Render an
AddressRowfor the wrap CW20 when a validated address is available:asset_address(CW20) orasset: { contract, denom }, filled from indexer hub config /assets.contract_addressforasset_id. Frontend must still pass it throughgetExplorerAddressUrl(never trust indexer to supplyjavascript:hrefs).USTC_C_TOKEN_ADDRESS/LUNC_C_TOKEN_ADDRESS(env overlay, same as wrap stack). Empty / invalid → omit the row (price may still show). Do not hardcode a Finder URL string.Copy cUSTC token contract/View cUSTC token contract on explorer(and cLUNC equivalents). Distinct from existingCopy UST1 source pair.data-testid:protocol-dex-hub-custc-tokenandprotocol-dex-hub-lunc-token(plus existingprotocol-dex-hub-{ticker}/-usd).2. LUNC / USD column
Add ticker
luncto the hub card allowlist (UI label LUNC / USD).Preferred data path (one query): extend indexer
hub_prices+HubTickerwithlunc:price_usd= latest #515 LUNC CEX oracle (same fail-closed as cUSTC).source_pair= null.asset_address= configured cLUNC wrap (newHUB_CLUNC_ADDRESS, LocalTerra override).hub_prices_ticker_chk; refresh loop writesluncon oracle tick (noswap_eventsscan).GET /api/v1/hub-pricestickersarray becomes["custc","lunc","ust1","ustr"]or documented order. Unknown path still 400.GET /oracle/price/luncunchanged (CEX history/sources). Do not drop the CEX LUNC tab.Acceptable smaller slice: frontend fetches
getOraclePrice('lunc')only for this cell and still uses allowlisted cLUNC wrap for the AddressRow. Must fold that query into Protocol outage detection. Do not callgetOraclePrice('lunc')with user-controlled ticker.Column order (recommended): cUSTC, LUNC, UST1, USTR (oracle-anchored wraps first, then DEX-derived hubs). Document in
docs/frontend.md.3. Docs / skills / verify
make verify-issue-<iid>wrapping hub ticker parse tests, Protocol RTL, indexer hub API (if extended), andmake verify-issue-556/550/515/541as regressions.Acceptance criteria
hrefisgetExplorerAddressUrlonly.source_pair.—, not$0/$1/ USTC price.ulunais not given a fabricated Finder URL.ustc|lunc|vfdusd). NogetOraclePrice('ustr'|'ust1'|'custc').?ticker=rejectjavascript:,../, unknown →ustc.GET /hub-prices/{ticker}still 400.GET /oracle/price/custc|ust1|ustrstill 400. Ifluncis added to hub-prices,GET /hub-prices/luncis the DEX-card snapshot, distinct from CEX/oracle/price/lunc.javascript:or open-redirecthref.volume_usd, UST1/USTR ranking, Charts USD,/ust1window unchanged. vFDUSD not used for this column.docs/frontend.md+docs/indexer-invariants.mdupdated;make verify-issue-<iid>exists and passes.Test plan — functional paths
Frontend RTL (
ProtocolPage.test.tsx/ hub ticker unit tests)terra1…cUSTC; copy payload is that contract, not the labelcUSTC.source_pair; cUSTC still has no source_pair row whensource_pair: null.price_usdformats withformatPairPrice($), notformatNumcompactT.≤0→—.getOraclePrice('ustr'|'ust1'|'custc').parseHubPriceTicker('lunc')(if added) accepts;javascript:,../ust1,LUNC\u200b,clunc(unless you explicitly alias — default nocluncpath alias) → null/400.VITE_INDEXER_URL/ host:port (#422).Indexer (if hub API extended;
make setup-indexer-postgres)GET /api/v1/hub-pricesincludesluncwithprice_usd= LUNC oracle,source_pairnull,asset_address= configured cLUNC.custcentry includesasset_address= configured cUSTC;source_pairstill null.lunc.price_usdnull (or omitted mark), not last USTC tick.GET /hub-prices/not-a-ticker400;GET /oracle/price/ustr400; catalog CEX still three tickers.lunc; refresh does not scanswap_eventson GET.api_hub_prices/ #556 tests still pass (UST1/USTR ranking, dust floor, symbol spoof).E2E / LocalTerra
/protocolhub card: four columns; cUSTC and LUNC token links open explorer (or LCD account URL onlocal) for wrap CW20s from.env.local.Test plan — attack, hack, and abuse
Treat explorer/copy chrome as a trust-boundary surface (#430, #378, H8).
Open redirect / XSS via addresses and tickers
asset_address/source_pair=javascript:alert(1),data:text/html,…,https://evil.example/,terra1+ HTML,../→ no<a href>. Copy either no-ops or copies only after bech32 allowlist.GET /hub-prices/{ticker}and?ticker=withlunc%2e%2e%2f,lunc%00, homoglyphlunc,LUNC, extra path segments → 400 or ignored; never concatenated into SQL orfetch(${base}/${user}).asset_addresspointing at a different CW20 than hub config must not win: display uses configured hub wrap, or omit. SymbolcUSTCon a gem CW20 must not appear as the cUSTC link.Spoof / thin-pool / oracle confusion
symbol=LUNCorsymbol=cUSTCcannot become the AddressRow target (contract allowlist only).custcinto CEXgetOraclePricefrom this card.$1must not appear as LUNC or cUSTC hub USD.Clickjacking / new-window
rel="noopener noreferrer"target="_blank". Nowindow.openwith attacker URL.Clipboard abuse
source_pairwhen the control is labeled token.Availability / DoS
swap_eventsaggregate on Protocol GET.LocalTerra / env injection
VITE_USTC_C_TOKEN_ADDRESS=' javascript:…'or non-bech32 → omit explorer. Do not interpolate env intohrefwithoutgetExplorerAddressUrl.Verification criteria
Done when:
make verify-issue-<iid>is green (new script + docs/skill grep as needed).make verify-issue-556,make verify-issue-550,make verify-issue-515,make verify-issue-541still green.cargo testhub/oracle API cases above with Postgres./protocol: four hub cells; cUSTC and LUNC explorer targets are wrap CW20s; UST1/USTR still show pair contracts; CEX tabs unchanged.Out of scope
volume_usd/ Charts candles //ust1.ulunaFinder URLs.marked as related to #556
marked as related to #550
marked as related to #515
marked as related to #541
mentioned in commit
a356757ff3mentioned in merge request !1106
Merged via !1106.
Merge with #568/#571 left
HubUsdConfig/ testAppStatemissingclunc_addressandhub_usd; those initializers were fixed on the branch before merge. Frontend RTL + Playwright protocol-page (5 workers) passed; hub/oracle integration tests passed after the fix.Remaining manual:
/protocolLocalTerra: four hub cells; cUSTC/LUNC explorer targets are wrap CW20s from.env.local; UST1/USTR still show pair contracts; CEX LUNC tab still worksmentioned in issue #583
marked as related to #583
mentioned in issue #589
mentioned in issue #682
mentioned in issue #683