feat: /pool Created column — relative age from indexer created_at #662
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#662
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
Fill the
/poolpairs-table Created column with a retail relative age (3 hours ago,12 days ago,2 years ago). Bundle indexer list JSON + sort alignment + dApp formatter/cell + docs/verify in one issue. Do not split “API field” vs “table cell” vs “relative-time helper.”Related: #547 (
/pooltable; P547-4 deferred Created to—), #655 (v2 LP USD column — does not own Created), #534 (catalog rank), #562 (hide gems), #489 (no lectures), #653 (no nested chrome).Out of scope:
/protocolpairs_added_30dcopy, GeckoTerminal/gt/pair timestamps (already exposecreatedAtBlockTimestampfrompairs.created_at), Charts “Created” sort dropdown (no Created column), reconstructing on-chain genesis from factory events / first-swapblock_timestamp.Current codebase
The Created header is sortable. The Created cell is a hardcoded em-dash. The timestamp already lives on
pairsin Postgres;GET /api/v1/pairsdrops it. This is intentional leftover from #547, not a CSS/empty-string glitch./poolcellPoolPairsTable.tsxdata-testid="pool-row-created"always renders—(lines ~232–234). HeadersortKey="created"/pool-sort-createdalready exists. Manage expandcolSpan={6}.docs/frontend.mdP547-4: “Created cells show—becauseGET /api/v1/pairslist JSON has no timestamp (sort still hits indexercreated).” Same sentence inskills/AGENTS_FRONTEND_POOL_TABLE.md.PoolPage.tsx+poolListQuery.ts: catalog defaultlimit=500sort=volume_24h+ client rank; column click sends indexersort=createdorder=desc(default for created). Production omits gems (P562-3).IndexerPairhaspair_address, assets,lp_token,fee_bps,is_active,code_id_frozen?,volume_quote_24h?. Nocreated_at.IndexerPairSortalready includes'created'.PairResponseomits timestamps.list_pairsmapsPairRow→ response and dropsp.created_at/p.created_at_block. Same struct forGET /api/v1/pairs/{addr}andGET /api/v1/tokens/{addr}/pairs.PairRow:created_at_block: Option<i64>,created_at: DateTime<Utc>(TIMESTAMPTZ NOT NULL DEFAULT NOW()). List SQL already SELECTs both.idx_pairs_created_blockexists.pair_discovery.rsupsert_pair(..., created_at_block = None). First insert stampscreated_at = NOW();ON CONFLICTdoes not refreshcreated_at. Socreated_at= indexer first-seen, not factoryCreatePairgenesis (same caveat as overviewpairs_added_30d/docs/runbooks/overview-global-stats-brin.md). After--fresh/ DB rebuild every pair looks new.sort=createdtodayORDER BY p.created_at_block … NULLS LAST, p.id ASC. Discovery never writescreated_at_block, so Created sort is effectively id order. Displayingcreated_atwhile sortingcreated_at_blockwould disagree.formatDate.tshasformatTime/formatDateTimeonly (clock / “Jan 15, 2:30 PM”). No “N hours/days/years ago” helper. Tape uses absoluteformatTime.indexer/src/api/gt.rs:createdAtBlockTimestamp: Some(pair.created_at.timestamp())— samepairs.created_at, different adapter. Official/api/v1/pairsmust grow its own field; do not scrape/gt/from the dApp.PoolPage.test.tsx/e2e/pool-table-547.spec.tsnever assert Created cells.indexer/tests/api_pairs.rsasserts list 200 + invalid sort 400; nocreated_atkey.Why this is needed
/poolsee a Created header and an empty age. Retail expectation is “how old is this pool?” — the same pattern as pair age on other DEX UIs. Hardcoded—reads as “unknown / broken,” not “intentionally deferred.”list_pairs_filteredSELECTsp.created_at. Shipping a second LCD/get_pairper row to invent a date would violate P547-9 / A8 (no N+1 on first paint).—. Align JSON + cell +ORDER BYso the caret matches what people see.Constraints / guardrails
—. Do not break existingPairResponseclients: additive optional/present ISO-8601 string.pairs.created_at(first-seen). Do not claim on-chain genesis in UI copy. Do not reconstruct age from first swap, LCDContractInfo, or factory event scans in this issue. After--fresh, all ages look recent — document for operators indocs/frontend.md/ playbook, not as a table lecture (#489).sort=createdwith the displayed clock. ChangeORDER BYfromp.created_at_blocktop.created_at(always NOT NULL) +p.idtiebreak. Default order remains desc (newest first). Catalog default (no column click) stays UST1-hub rank — no catalog overlay on Created. Search staysrelevance.getPairs.5 minutes ago,3 hours ago,12 days ago,2 years ago. Singular/plural. Missing / unparsable / non-finite /Invalid Date→—. Do not print raw ISO in the cell. Optionaltitle= absolute UTC (orformatDateTime) for hover — not a paragraph.Date.now(); React Query 30s stale is enough. Do notsetIntervalper row.created_atis a timestamp string. Text node / React children only.javascript:, HTML, extremely large years, unix-ms vs unix-s confusion →—.just now/1 minute ago. Far-future / year 0 / negative →—.PairResponsebuilders (list_pairs,get_pair,get_token_pairs) soIndexerPairstays one shape. Optionalcreated_at_blockis nice for integrators; not required for the cell.PAIR_LIST_LIMIT_MAXor change catalog fetch for this column. Do not renamevolume_quote_24h. Do not implement #655liquidity_usdhere.card-glassper row. Keepoverflow-x-auto. Light + dark. Phone 375px: horizontal scroll OK; cell must not inject a wrapping lecture.aria-sort.data-testid="pool-row-created"stays. Visible text is the relative phrase (or—)./gt/field names unless a shared formatter is extracted (allowed). Official dApp must not call/gt/to fill/pool.Relevant files
indexer/src/api/pairs.rsPairResponse, list / get mappingindexer/src/api/tokens.rsget_token_pairssame structindexer/src/db/queries/pairs.rsPairRow,sort=createdORDER BYindexer/tests/api_pairs.rscreated_at+ sort orderfrontend-dapp/src/types/index.tsIndexerPair.created_at?frontend-dapp/src/utils/formatDate.tsformatRelativeAge(or equivalent)frontend-dapp/src/utils/__tests__/formatDate.test.tsfrontend-dapp/src/components/pool/PoolPairsTable.tsxfrontend-dapp/src/pages/PoolPage.tsxfrontend-dapp/src/pages/PoolPage.test.tsxfrontend-dapp/src/utils/__tests__/poolListQuery.test.tscreatedstill in allowlist / default descfrontend-dapp/e2e/pool-table-547.spec.ts—when indexer has datesdocs/frontend.mdskills/AGENTS_FRONTEND_POOL_TABLE.mddocs/indexer-invariants.mdcreated_atRecommended direction
created_at: String(RFC3339 / serdeDateTime<Utc>) onPairResponse, sourced fromp.created_at. Keep it on every constructor of that struct.PairListSort::CreatedtoORDER BY p.created_at {ASC\|DESC}, p.id ASC.created_at?: stringonIndexerPair(optional so mocks / old payloads still type-check).formatRelativeAge(iso: string \| null \| undefined, nowMs?: number): stringinformatDate.ts— injectnowin tests. Return—on failure.PoolPairsTable:{formatRelativeAge(ip.created_at)}withtitle= absolute time when valid.created_at;—only when missing/invalid. Note first-seen (not genesis) in the docs table, not the page.make verify-issue-547or addmake verify-issue-NNNthat covers Created cells + formatter unit tests. Prefer extending 547 plus a focused verify if the new indexer assertion needs Postgres.Acceptance criteria
GET /api/v1/pairsitems includecreated_atas an ISO-8601 UTC timestamp for seeded pairs.GET /api/v1/pairs/{addr}andGET /api/v1/tokens/{addr}/pairsinclude the same field (onePairResponseshape).sort=created&order=descorders bycreated_atnewest first;ascoldest first; invalidsortstill 400./poolCreated cells show relative age (N minutes/hours/days/years ago) whencreated_atis valid.created_at→—(no crash, no raw dump).relevance; gems still omitted in production (P562-3).titlemay show absolute time; visible cell is relative (or—).colSpanstill matches column count.Test plan (all paths)
Indexer
list_pairs_returns_200(or sibling): each item hascreated_atmatching the seed row’s RFC3339 (or parseable UTC).created_at:sort=created&order=desc→ newer first;asc→ older first.sort=createdwith one very old and oneNOW()row (SQLUPDATE pairs SET created_at = …).GET /api/v1/pairs/{addr}and token-pairs includecreated_at.sort=bad_sort→ 400;offsetover max → 400 (no regression).EXPLAINoptional:sort=createdusespairs.created_at(seq scan on tiny test DB is OK; do not add a liveSUM).Formatter (Vitest)
now: 30s →just nowor1 minute ago(pick one, lock it).null/undefined/''/'not-a-date'/'<img>'/'javascript:alert(1)'→—.—. Near future (now + 30s) →just now/ minute bucket (not “in 30 seconds” unless you explicitly support that; prefer not).'1710000000000') →—or documented parse; do not show “54000 years ago.”dApp unit
PoolPage.test.tsx: mock items withcreated_at→pool-row-createdmatches helper output (e.g.3 hours ago).created_at→—.pool-sort-created→getPairscalled withsort=createdorder=desc; second clickasc.aria-sortisnoneuntil click.poolListQuerystill defaultscreated→desc; allowlist unchanged except docs.E2E / verify
e2e/pool-table-547.spec.ts(or new spec): first Created cell is not universally—when LocalTerra indexer has pairs (skip-soft if indexer down, same pattern as other pool specs).make verify-issue-547still green; new/extended verify covers formatter + list JSON.Test plan (attack, hack, and abuse)
created_at=<script>alert(1)</script>,"><img src=x onerror=alert(1)>,javascript:alert(1). Cell is text; no attribute breakout intitle.+275760-09-13(JS Date max), year0000,NaN,Infinity,nullJSON. Cell—, no throw on list render.__proto__,constructor) on a pair item must not changeObject.prototypeor the formatter.sort=created;drop/created_at/ SQL fragments still 400 (existing allowlist).getPairs; no N+1getPairfor dates.message/symbolfield. Pair symbol spoof cannot appear inside the Created cell.—, not a crash that blanks the whole table.Zvs offset+00:00both parse. Do not treat local midnight as a second-day jump in tests (injectnow)./poolpaint still must not callgetPool/getPairFeeConfigper row.Verification criteria
Done when:
/pool(LocalTerra or production indexer that emitscreated_at) sees Created cells like6 hours ago/3 days ago/1 year ago, not a full column of—.GET /api/v1/pairsJSON includescreated_at;make test-frontendformatter + PoolPage tests pass; indexerapi_pairs(or added test) passes withmake setup-indexer-postgres+ targetedcargo test.—.marked as related to #547
marked as related to #655
mentioned in commit
e76b920897mentioned in merge request !1165
mentioned in commit
638e9ca59cmentioned in commit
4da8ef0ae3mentioned in commit
d52bdf039aMerged to
mainvia !1165.PairResponsekeeps bothcreated_atandliquidity_usd(filled by #655 JOIN).Leftover:
/poolCreated column shows relative age after indexer+frontend rebuild.make verify-issue-662.mentioned in issue #655
mentioned in issue #673
mentioned in issue #692