feat(frontend): Swap extreme-size / unfunded pay guidance (acquire UST1 + retail slippage) #678

Closed
opened 2026-08-27 00:17:50 +00:00 by PlasticDigits · 21 comments
PlasticDigits commented 2026-08-27 00:17:50 +00:00 (Migrated from gitlab.com)

Summary

Swap still quotes oversized, unfunded pay amounts as if they were executable. Community testers type values they do not hold (example: 50,000 UST1 → LUNC), see a live-looking You Receive (example: 1.587M LUNC, route UST1 → cLUNC → LUNC), and a red High expected slippage banner. Wallet is often disconnected. After connect the CTA becomes Insufficient Balance with no next step.

Suggested product direction (illustrative, not shipping copy): a short blocking dialog such as “You don’t have 50k UST1 — you need about N more vFDUSD on UST1 to start this trade” plus a Guide button. Bundle the acquire path, retail slippage wording, and window-cap honesty into one Swap/Trade change.

Current codebase

Quote theater is not gated on balance or connect

SwapPage.tsx runs simQuery whenever hasRoute && isPositiveDecimalAmount(debouncedInputAmount). Wallet is not required. balanceQuery is enabled: !!address && !!offerAssetInfo, so a disconnected session never computes insufficientBalance.

Connected shortfall only flips the CTA:

  • insufficientBalance → button Insufficient Balance, disabled
  • No banner, no acquire CTA, no deep-link to /ust1 or /wrap
  • The quote card (Route / Min received / receive amount) stays on screen

Disconnected CTA is Connect Wallet. First click opens the wallet modal. If expected slippage is > 5%, a second click is required (showImpactConfirm) with “High expected slippage warning / {n}% expected slippage — click again to confirm.” That two-step only runs after connect and after the insufficient-balance branch is skipped.

Slippage copy is jargon; extreme gate is 30% / 99%

Signal Threshold UI today
High expected slippage banner priceImpact > 5 “The quoted route deviates significantly from fair cross-rate token prices.”
Confirm-again CTA same > 5 “Confirm Swap ({n}% impact)”
Submit block > SWAP_EXPERT_MODE_SLIPPAGE_BLOCK_PCT (30) unless Expert Mode “Slippage is too high” + Enable Expert Mode (#293)
Extreme alert >= SWAP_EXTREME_SLIPPAGE_WARNING_PCT (99) Always shown, even with Expert Mode

Constants and helpers: swapRouteSlippage.ts. Retail vs Settings label: slippageProtectionCopy.ts. Invariants: docs/swap-max-spread-ux.md.

A 50k UST1 → LUNC quote that lands between 5% and 30% shows the red banner but is not blocked. In a disconnected session there is no balance row (AmountBalanceActions returns null when !walletConnected), so nothing contradicts the fantasy size.

UST1 acquire path exists — Swap does not point at it

Oracle mint/redeem is /ust1 only (#506, U1–U8):

File Role
Ust1Page.tsx Deposit / Withdraw. No useSearchParams. Lead already says this is not an AMM swap.
ust1Window.ts LCD effective_swap + CW20 Send
ust1WindowMath.ts INV-SWAP-001/002 integer quotes. Deposit is vFDUSD → UST1. No inverse (UST1 target → vFDUSD in).
ust1WindowGates.ts Pause / stale / per-tx / rolling 24h / insufficient balance
ust1SecondaryMarket.ts U1: AMM must never be marketed as mint/redeem. ust1SecondarySwapPath() is / only — Swap does not honor token query params.

Published window caps (docs/runbooks/ust1-window-ui.md): fee_bps=100, per-tx 1,000 UST1, rolling 24h 10,000 UST1. A 50,000 UST1 pay cannot be minted in one window tx or one rolling day. /ust1 insufficient copy is only “Balance is too low for this amount.”

Wrap natives: WrapPage.tsx same Insufficient Balance dead-end. Swap already auto-wraps/unwraps on the UST1 → cLUNC → LUNC route; that is not how you obtain UST1.

Trade market

TradeMarketOrderPanel.tsx quotes without a dedicated insufficient-balance CTA. Disconnected label is Connect Wallet. No UST1 acquire guidance. Same helper must cover Trade market so Swap and Trade do not diverge.

Copy / cognitive load

#489 / AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md: blocking errors ≤ one short sentence + optional Docs/guide control. No always-on lectures, no “oracle mint/redeem” essays on Swap. Do not add a new marketing guide site; /ust1 is the guide.

Why the new implementation is needed

  1. Screenshot theater. Disconnected / unfunded quotes look like a real offer (large LUNC out + Route + Min received). Community members share them as if the trade is available.
  2. Dead-end after connect. Insufficient Balance does not say which token is short, how to get it, or that UST1 comes from vFDUSD on /ust1 — not from swapping vFDUSD on the AMM.
  3. Window math vs wish size. Suggesting “get 49,999 vFDUSD and trade 50k UST1” would be wrong: window per-tx / rolling caps reject that mint. Guidance must be honest about remaining capacity.
  4. Jargon slippage. “Fair cross-rate token prices” does not tell a retail user to type a smaller amount. The 5% banner and 30% Expert Mode gate stay; the wording and next action need to be human.
  5. U1 safety. A naive “swap vFDUSD to begin trading” CTA would send users into a thin AMM hop and violate U1 (#508).

This is a frontend product change. Do not change pair/router max_spread, indexer route/solve, or window contract limits.

Constraints / guardrails

  • Do not weaken Expert Mode (>30% block), per-hop max_spread preflight, or the >5% confirm-again gate (#293, #134, #497).
  • Do not describe AMM Swap/Trade as mint or redeem (U1). Forbidden phrases live in UST1_AMM_AS_MINT_FORBIDDEN. Guide target for UST1 shortfall is /ust1, never “Swap vFDUSD → UST1”.
  • Do not invent FX. vFDUSD needed = inverse of depositVfdusdToUst1 using live effective_swap.oracle.rate + fee_bps. Fail closed if window env is off, query fails, rate is 0, or oracle is paused/stale.
  • Cap suggestions to min(pay_shortfall_ust1, per_tx_ust1_limit, rollingRemainingUst1). If typed pay exceeds remaining window capacity, say the window cannot mint that size; do not promise 50k UST1.
  • #489: one sentence + Guide / Reduce amount. No protocol essay on the Swap card.
  • Same-origin only for Guide (/ust1, /wrap). No third-party URLs, no javascript:, no window.location built from token symbols.
  • No HTML interpolation of amounts/symbols. Text nodes / React children only.
  • Do not auto-switch pay token to vFDUSD or auto-submit wrap/window.
  • Do not require a new retail docs site. Optional docs/ note for agents is fine; the in-app Guide is /ust1.
  • Swap still must not honor arbitrary token query params unless that work is in-scope here and tested (today ust1SecondarySwapPath() is / only).
  • In-app / WebView sessions (wallet not injected) stay on Connect Wallet; do not pretend a balance exists.

Relevant files

Path Why
frontend-dapp/src/pages/SwapPage.tsx Quote, insufficient CTA, slippage banners, confirm-again
frontend-dapp/src/utils/swapRouteSlippage.ts 5 / 30 / 99 thresholds
frontend-dapp/src/utils/slippageProtectionCopy.ts Retail slippage strings
frontend-dapp/src/components/swap/ExpertModeModal.tsx Expert Mode confirm
frontend-dapp/src/components/common/AmountBalanceActions.tsx Balance/Max hidden when disconnected
frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx Same quote theater on /trade
frontend-dapp/src/pages/Ust1Page.tsx Guide target; needs optional deposit prefill
frontend-dapp/src/utils/ust1WindowMath.ts Add tested inverse deposit
frontend-dapp/src/utils/ust1WindowGates.ts Caps / pause / remaining
frontend-dapp/src/utils/ust1SecondaryMarket.ts U1 copy asserts
frontend-dapp/src/pages/WrapPage.tsx Optional: same shortfall helper for wrap-native pay
docs/swap-max-spread-ux.md Slippage invariants
docs/runbooks/ust1-window-ui.md Window caps / U1–U8
skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md Copy rules
skills/AGENTS_UST1_WINDOW_UI.md Window playbook
docs/frontend.md Retail copy + slippage sections
  1. Pure helper (e.g. swapPayAcquireGuidance.ts) from { payAsset, payRaw, payBalanceRaw, vfdusdBalanceRaw, windowView | null, wrapEnabled } → discriminated result:
    • ok
    • disconnected_quote — quote-only; CTA remains Connect
    • insufficient_generic — “You don’t have enough {symbol}.”
    • insufficient_ust1_window — shortfall + capped vFDUSD-in + /ust1 href
    • insufficient_ust1_over_window — typed size exceeds per-tx or rolling remaining
    • insufficient_wrap — pay is native/wrapped and /wrap is the acquire path
    • high_impact — expected slippage > 5% and funded: “This size moves the pool. Try a smaller amount.” + optional Reduce
  2. Swap + Trade market consume the helper. Replace bare Insufficient Balance with the one-sentence status + Guide button (data-testids below).
  3. /ust1 prefill: ?direction=deposit&amount= human vFDUSD, sanitized (isPositiveDecimalAmount), clamped to per-tx / rolling before apply. Ignore unknown params.
  4. Inverse math in ust1WindowMath.ts with bigint ceil; unit tests vs INV-SWAP-001.
  5. High-impact copy for the existing >5% banner: size / pool movement, not “fair cross-rate”. Keep numeric % in Trade details. Optional “Use {human} instead” that sets pay to a conservative fraction of typed amount or of wallet spendable — never above balance.
  6. Disconnected: keep quoting (useful) but mark receive as quote-only (e.g. data-testid="swap-quote-only"). Do not show Min received as a promise. Do not run confirm-again while disconnected.
  7. U1 test: assertSecondaryMarketCopy on every new string.

Suggested shipping shape (not literal): “You don’t have 50,000 UST1. Deposit about 1,000 vFDUSD on UST1 (window max this tx).” [Get UST1] → /ust1?direction=deposit&amount=1000 (amount from inverse + caps, not a hardcoded 1000).

Acceptance criteria

  • A1 Disconnected + positive pay + settled quote: Connect Wallet CTA; receive marked quote-only; no confirm-again; no fake balance.
  • A2 Connected + pay raw > LCD balance: submit stays disabled; one-sentence shortfall; no executable Swap.
  • A3 Pay is UST1, window enabled, shortfall ≤ remaining caps: Guide → /ust1?direction=deposit&amount={capped human vFDUSD} using inverse effective_swap math.
  • A4 Pay is UST1 and typed shortfall exceeds per-tx or rolling remaining: copy states the window cannot mint that size; Guide still opens /ust1; no “deposit 49,999 vFDUSD” that would fail on-chain.
  • A5 Window env off / query fail / stale / paused: generic insufficient + no invented vFDUSD number.
  • A6 Copy never implies AMM mint/redeem (U1).
  • A7 >5% expected slippage, funded trade: retail size warning + existing confirm-again; 30% / 99% / Expert Mode unchanged.
  • A8 Trade market uses the same helper for insufficient / UST1 Guide.
  • A9 #489: no new always-on lecture on Swap. Guide is a control, not a paragraph stack.
  • A10 /ust1 ignores hostile query strings (XSS, huge exponents, negative, non-decimal).
  • A11 Docs: short note on docs/swap-max-spread-ux.md + docs/runbooks/ust1-window-ui.md (Swap → window acquire). No new retail microsite.

Test plan (all paths)

Unit (Vitest)

  • Inverse deposit: known rate/fee fixtures; ceil; rate=0 / bad fee → null.
  • Helper matrix: disconnected; funded; UST1 shortfall under cap; UST1 over per-tx; UST1 over rolling; window null; wrap-native shortfall; generic CW20 shortfall; high impact funded vs unfunded (unfunded must not prefer high-impact over insufficient).
  • assertSecondaryMarketCopy on all new retail strings.
  • /ust1 query parse: empty, amount=abc, 1e99, negative, extra keys, valid human clamped to remaining.

Component

  • SwapPage: disconnected 50k UST1 fixture shows quote-only + Connect; connected 0 balance shows insufficient + Guide; over-window fixture shows over-capacity copy; funded >5% / >30% / ≥99% banners unchanged in gate behavior.
  • TradeMarketOrderPanel: same insufficient + Guide.
  • Ust1Page: prefill deposit amount; clamp; reject junk params.

E2E / LocalTerra (when window + wrap env present)

  • Connected wallet with 0 UST1, typed 50k: cannot submit; Guide lands on /ust1 with clamped amount.
  • Deposit happy path still works without query params (e2e/ust1-window.spec.ts).
  • Funded small swap still submits (no acquire chrome).
  • Expert Mode path unchanged (swap-enable-expert-mode).

Regression: make test-frontend, make lint-frontend, make verify-issue-506, Swap slippage tests in SwapPage.test.tsx / swapRouteSlippage.test.ts.

Test plan (attack, hack, abuse)

Vector Expect
T1 XSS in amount/symbol (<img>, javascript:) Never dangerouslySetInnerHTML; href allowlisted to /ust1 or /wrap + safe query
T2 Open redirect Guide ?next= Ignore; only built paths
T3 Query injection /ust1?amount=999999999999999 Clamp to per-tx / rolling; invalid → ignore
T4 Spoofed balance Only LCD getTokenBalance for connected address; no indexer “portfolio” as spendable
T5 Invented oracle No fallback 1:1; fail closed if effective_swap missing
T6 U1 bait No CTA that sets pay=vFDUSD and receive=UST1 on Swap
T7 Window grief Suggested deposit ≤ remaining; do not loop users into failing Send
T8 Slippage bypass Acquire UI must not skip 30% Expert Mode or hop max_spread
T9 Screenshot / social Disconnected receive is visibly quote-only (not a signed offer)
T10 Token spoof Pay identity is contract addr (UST1_TOKEN_ADDRESS), not ticker string
T11 Prefill drain Prefill never auto-submits window or swap
T12 WebView No wallet → Connect only; no invented “you have 0” vs “you have …”

Verification criteria

  • Manual columbus-5 or LocalTerra with window pins: disconnected 50k UST1 → LUNC looks like a quote, not a trade; connect with empty UST1 → Guide → /ust1 with a legal deposit amount (≤ 1,000 UST1 notional / remaining 24h).
  • Same wallet with enough UST1 for a small funded swap: no acquire dialog; existing slippage confirm still works.
  • make verify-issue-506 still green. New make verify-issue-<iid> covers helper + Swap/Trade/Ust1 tests listed above.
  • Code review: no AMM-as-mint copy; no git commit --no-verify; no secrets in fixtures.
  • #293 Expert Mode / expected slippage
  • #489 retail copy
  • #506 /ust1 window UI
  • #508 U1 secondary AMM
  • #497 5% default protection
  • #134 max spread UX
## Summary Swap still quotes **oversized, unfunded** pay amounts as if they were executable. Community testers type values they do not hold (example: **50,000 UST1 → LUNC**), see a live-looking **You Receive** (example: **1.587M LUNC**, route **UST1 → cLUNC → LUNC**), and a red **High expected slippage** banner. Wallet is often **disconnected**. After connect the CTA becomes **Insufficient Balance** with no next step. Suggested product direction (illustrative, not shipping copy): a short blocking dialog such as “You don’t have 50k UST1 — you need about N more vFDUSD on UST1 to start this trade” plus a **Guide** button. Bundle the acquire path, retail slippage wording, and window-cap honesty into **one** Swap/Trade change. ## Current codebase ### Quote theater is not gated on balance or connect [`SwapPage.tsx`](frontend-dapp/src/pages/SwapPage.tsx) runs `simQuery` whenever `hasRoute && isPositiveDecimalAmount(debouncedInputAmount)`. Wallet is **not** required. `balanceQuery` is `enabled: !!address && !!offerAssetInfo`, so a disconnected session never computes `insufficientBalance`. Connected shortfall only flips the CTA: - `insufficientBalance` → button **Insufficient Balance**, disabled - No banner, no acquire CTA, no deep-link to `/ust1` or `/wrap` - The quote card (Route / Min received / receive amount) **stays on screen** Disconnected CTA is **Connect Wallet**. First click opens the wallet modal. If expected slippage is **> 5%**, a second click is required (`showImpactConfirm`) with “High expected slippage warning / {n}% expected slippage — click again to confirm.” That two-step only runs **after** connect and after the insufficient-balance branch is skipped. ### Slippage copy is jargon; extreme gate is 30% / 99% | Signal | Threshold | UI today | |--------|-----------|----------| | High expected slippage banner | `priceImpact > 5` | “The quoted route deviates significantly from fair cross-rate token prices.” | | Confirm-again CTA | same `> 5` | “Confirm Swap ({n}% impact)” | | Submit block | `> SWAP_EXPERT_MODE_SLIPPAGE_BLOCK_PCT` (**30**) unless Expert Mode | “Slippage is too high” + Enable Expert Mode ([#293](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/293)) | | Extreme alert | `>= SWAP_EXTREME_SLIPPAGE_WARNING_PCT` (**99**) | Always shown, even with Expert Mode | Constants and helpers: [`swapRouteSlippage.ts`](frontend-dapp/src/utils/swapRouteSlippage.ts). Retail vs Settings label: [`slippageProtectionCopy.ts`](frontend-dapp/src/utils/slippageProtectionCopy.ts). Invariants: [`docs/swap-max-spread-ux.md`](docs/swap-max-spread-ux.md). A 50k UST1 → LUNC quote that lands **between 5% and 30%** shows the red banner but is **not** blocked. In a disconnected session there is no balance row (`AmountBalanceActions` returns null when `!walletConnected`), so nothing contradicts the fantasy size. ### UST1 acquire path exists — Swap does not point at it Oracle mint/redeem is **`/ust1`** only ([#506](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/506), **U1–U8**): | File | Role | |------|------| | [`Ust1Page.tsx`](frontend-dapp/src/pages/Ust1Page.tsx) | Deposit / Withdraw. **No** `useSearchParams`. Lead already says this is not an AMM swap. | | [`ust1Window.ts`](frontend-dapp/src/services/terraclassic/ust1Window.ts) | LCD `effective_swap` + CW20 `Send` | | [`ust1WindowMath.ts`](frontend-dapp/src/utils/ust1WindowMath.ts) | INV-SWAP-001/002 integer quotes. Deposit is vFDUSD → UST1. **No inverse** (UST1 target → vFDUSD in). | | [`ust1WindowGates.ts`](frontend-dapp/src/utils/ust1WindowGates.ts) | Pause / stale / **per-tx** / **rolling 24h** / insufficient balance | | [`ust1SecondaryMarket.ts`](frontend-dapp/src/utils/ust1SecondaryMarket.ts) | **U1**: AMM must never be marketed as mint/redeem. `ust1SecondarySwapPath()` is `/` only — Swap does **not** honor token query params. | Published window caps ([`docs/runbooks/ust1-window-ui.md`](docs/runbooks/ust1-window-ui.md)): `fee_bps=100`, **per-tx 1,000 UST1**, **rolling 24h 10,000 UST1**. A 50,000 UST1 pay **cannot** be minted in one window tx or one rolling day. `/ust1` insufficient copy is only “Balance is too low for this amount.” Wrap natives: [`WrapPage.tsx`](frontend-dapp/src/pages/WrapPage.tsx) same **Insufficient Balance** dead-end. Swap already auto-wraps/unwraps on the UST1 → cLUNC → LUNC route; that is **not** how you obtain UST1. ### Trade market [`TradeMarketOrderPanel.tsx`](frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx) quotes without a dedicated insufficient-balance CTA. Disconnected label is **Connect Wallet**. No UST1 acquire guidance. Same helper must cover Trade market so Swap and Trade do not diverge. ### Copy / cognitive load [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/489) / [`AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md`](skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md): blocking errors ≤ **one short sentence** + optional Docs/guide control. No always-on lectures, no “oracle mint/redeem” essays on Swap. Do **not** add a new marketing guide site; **`/ust1` is the guide**. ## Why the new implementation is needed 1. **Screenshot theater.** Disconnected / unfunded quotes look like a real offer (large LUNC out + Route + Min received). Community members share them as if the trade is available. 2. **Dead-end after connect.** **Insufficient Balance** does not say which token is short, how to get it, or that UST1 comes from vFDUSD on `/ust1` — not from swapping vFDUSD on the AMM. 3. **Window math vs wish size.** Suggesting “get 49,999 vFDUSD and trade 50k UST1” would be **wrong**: window per-tx / rolling caps reject that mint. Guidance must be honest about remaining capacity. 4. **Jargon slippage.** “Fair cross-rate token prices” does not tell a retail user to **type a smaller amount**. The 5% banner and 30% Expert Mode gate stay; the wording and next action need to be human. 5. **U1 safety.** A naive “swap vFDUSD to begin trading” CTA would send users into a thin AMM hop and violate **U1** ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508)). This is a **frontend product** change. Do not change pair/router `max_spread`, indexer `route/solve`, or window contract limits. ## Constraints / guardrails - **Do not weaken** Expert Mode (>30% block), per-hop `max_spread` preflight, or the >5% confirm-again gate ([#293](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/293), [#134](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/134), [#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497)). - **Do not** describe AMM Swap/Trade as mint or redeem (**U1**). Forbidden phrases live in `UST1_AMM_AS_MINT_FORBIDDEN`. Guide target for UST1 shortfall is **`/ust1`**, never “Swap vFDUSD → UST1”. - **Do not invent FX.** vFDUSD needed = inverse of `depositVfdusdToUst1` using live `effective_swap.oracle.rate` + `fee_bps`. Fail closed if window env is off, query fails, rate is 0, or oracle is paused/stale. - **Cap suggestions** to `min(pay_shortfall_ust1, per_tx_ust1_limit, rollingRemainingUst1)`. If typed pay **exceeds** remaining window capacity, say the window cannot mint that size; do **not** promise 50k UST1. - **#489:** one sentence + Guide / Reduce amount. No protocol essay on the Swap card. - **Same-origin only** for Guide (`/ust1`, `/wrap`). No third-party URLs, no `javascript:`, no `window.location` built from token symbols. - **No HTML interpolation** of amounts/symbols. Text nodes / React children only. - **Do not auto-switch** pay token to vFDUSD or auto-submit wrap/window. - **Do not** require a new retail docs site. Optional `docs/` note for agents is fine; the in-app Guide is `/ust1`. - Swap still must **not** honor arbitrary token query params unless that work is in-scope here and tested (today `ust1SecondarySwapPath()` is `/` only). - In-app / WebView sessions (wallet not injected) stay on **Connect Wallet**; do not pretend a balance exists. ## Relevant files | Path | Why | |------|-----| | `frontend-dapp/src/pages/SwapPage.tsx` | Quote, insufficient CTA, slippage banners, confirm-again | | `frontend-dapp/src/utils/swapRouteSlippage.ts` | 5 / 30 / 99 thresholds | | `frontend-dapp/src/utils/slippageProtectionCopy.ts` | Retail slippage strings | | `frontend-dapp/src/components/swap/ExpertModeModal.tsx` | Expert Mode confirm | | `frontend-dapp/src/components/common/AmountBalanceActions.tsx` | Balance/Max hidden when disconnected | | `frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx` | Same quote theater on `/trade` | | `frontend-dapp/src/pages/Ust1Page.tsx` | Guide target; needs optional deposit prefill | | `frontend-dapp/src/utils/ust1WindowMath.ts` | Add tested inverse deposit | | `frontend-dapp/src/utils/ust1WindowGates.ts` | Caps / pause / remaining | | `frontend-dapp/src/utils/ust1SecondaryMarket.ts` | U1 copy asserts | | `frontend-dapp/src/pages/WrapPage.tsx` | Optional: same shortfall helper for wrap-native pay | | `docs/swap-max-spread-ux.md` | Slippage invariants | | `docs/runbooks/ust1-window-ui.md` | Window caps / U1–U8 | | `skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md` | Copy rules | | `skills/AGENTS_UST1_WINDOW_UI.md` | Window playbook | | `docs/frontend.md` | Retail copy + slippage sections | ## Recommended direction 1. **Pure helper** (e.g. `swapPayAcquireGuidance.ts`) from `{ payAsset, payRaw, payBalanceRaw, vfdusdBalanceRaw, windowView | null, wrapEnabled }` → discriminated result: - `ok` - `disconnected_quote` — quote-only; CTA remains Connect - `insufficient_generic` — “You don’t have enough {symbol}.” - `insufficient_ust1_window` — shortfall + **capped** vFDUSD-in + `/ust1` href - `insufficient_ust1_over_window` — typed size exceeds per-tx or rolling remaining - `insufficient_wrap` — pay is native/wrapped and `/wrap` is the acquire path - `high_impact` — expected slippage > 5% **and** funded: “This size moves the pool. Try a smaller amount.” + optional Reduce 2. **Swap + Trade market** consume the helper. Replace bare **Insufficient Balance** with the one-sentence status + Guide button (`data-testid`s below). 3. **`/ust1` prefill:** `?direction=deposit&amount=` human vFDUSD, sanitized (`isPositiveDecimalAmount`), clamped to per-tx / rolling **before** apply. Ignore unknown params. 4. **Inverse math** in `ust1WindowMath.ts` with bigint ceil; unit tests vs INV-SWAP-001. 5. **High-impact copy** for the existing >5% banner: size / pool movement, not “fair cross-rate”. Keep numeric % in Trade details. Optional “Use {human} instead” that sets pay to a conservative fraction of typed amount or of wallet spendable — **never** above balance. 6. **Disconnected:** keep quoting (useful) but mark receive as quote-only (e.g. `data-testid="swap-quote-only"`). Do not show Min received as a promise. Do not run confirm-again while disconnected. 7. **U1 test:** `assertSecondaryMarketCopy` on every new string. Suggested shipping shape (not literal): “You don’t have 50,000 UST1. Deposit about 1,000 vFDUSD on UST1 (window max this tx).” **[Get UST1]** → `/ust1?direction=deposit&amount=1000` (amount from inverse + caps, not a hardcoded 1000). ## Acceptance criteria - [ ] **A1** Disconnected + positive pay + settled quote: Connect Wallet CTA; receive marked quote-only; no confirm-again; no fake balance. - [ ] **A2** Connected + pay raw > LCD balance: submit stays disabled; one-sentence shortfall; no executable Swap. - [ ] **A3** Pay is UST1, window enabled, shortfall ≤ remaining caps: Guide → `/ust1?direction=deposit&amount={capped human vFDUSD}` using inverse `effective_swap` math. - [ ] **A4** Pay is UST1 and typed shortfall **exceeds** per-tx or rolling remaining: copy states the window cannot mint that size; Guide still opens `/ust1`; **no** “deposit 49,999 vFDUSD” that would fail on-chain. - [ ] **A5** Window env off / query fail / stale / paused: generic insufficient + no invented vFDUSD number. - [ ] **A6** Copy never implies AMM mint/redeem (**U1**). - [ ] **A7** >5% expected slippage, **funded** trade: retail size warning + existing confirm-again; 30% / 99% / Expert Mode unchanged. - [ ] **A8** Trade market uses the same helper for insufficient / UST1 Guide. - [ ] **A9** `#489`: no new always-on lecture on Swap. Guide is a control, not a paragraph stack. - [ ] **A10** `/ust1` ignores hostile query strings (XSS, huge exponents, negative, non-decimal). - [ ] **A11** Docs: short note on `docs/swap-max-spread-ux.md` + `docs/runbooks/ust1-window-ui.md` (Swap → window acquire). No new retail microsite. ## Test plan (all paths) ### Unit (Vitest) - Inverse deposit: known rate/fee fixtures; ceil; `rate=0` / bad fee → null. - Helper matrix: disconnected; funded; UST1 shortfall under cap; UST1 over per-tx; UST1 over rolling; window null; wrap-native shortfall; generic CW20 shortfall; high impact funded vs unfunded (unfunded must **not** prefer high-impact over insufficient). - `assertSecondaryMarketCopy` on all new retail strings. - `/ust1` query parse: empty, `amount=abc`, `1e99`, negative, extra keys, valid human clamped to remaining. ### Component - `SwapPage`: disconnected 50k UST1 fixture shows quote-only + Connect; connected 0 balance shows insufficient + Guide; over-window fixture shows over-capacity copy; funded >5% / >30% / ≥99% banners unchanged in gate behavior. - `TradeMarketOrderPanel`: same insufficient + Guide. - `Ust1Page`: prefill deposit amount; clamp; reject junk params. ### E2E / LocalTerra (when window + wrap env present) - Connected wallet with 0 UST1, typed 50k: cannot submit; Guide lands on `/ust1` with clamped amount. - Deposit happy path still works without query params (`e2e/ust1-window.spec.ts`). - Funded small swap still submits (no acquire chrome). - Expert Mode path unchanged (`swap-enable-expert-mode`). Regression: `make test-frontend`, `make lint-frontend`, `make verify-issue-506`, Swap slippage tests in `SwapPage.test.tsx` / `swapRouteSlippage.test.ts`. ## Test plan (attack, hack, abuse) | Vector | Expect | |--------|--------| | **T1 XSS** in amount/symbol (`<img>`, `javascript:`) | Never `dangerouslySetInnerHTML`; href allowlisted to `/ust1` or `/wrap` + safe query | | **T2 Open redirect** Guide `?next=` | Ignore; only built paths | | **T3 Query injection** `/ust1?amount=999999999999999` | Clamp to per-tx / rolling; invalid → ignore | | **T4 Spoofed balance** | Only LCD `getTokenBalance` for connected address; no indexer “portfolio” as spendable | | **T5 Invented oracle** | No fallback 1:1; fail closed if `effective_swap` missing | | **T6 U1 bait** | No CTA that sets pay=vFDUSD and receive=UST1 on Swap | | **T7 Window grief** | Suggested deposit ≤ remaining; do not loop users into failing `Send` | | **T8 Slippage bypass** | Acquire UI must not skip 30% Expert Mode or hop `max_spread` | | **T9 Screenshot / social** | Disconnected receive is visibly quote-only (not a signed offer) | | **T10 Token spoof** | Pay identity is contract addr (`UST1_TOKEN_ADDRESS`), not ticker string | | **T11 Prefill drain** | Prefill never auto-submits window or swap | | **T12 WebView** | No wallet → Connect only; no invented “you have 0” vs “you have …” | ## Verification criteria - Manual columbus-5 or LocalTerra with window pins: disconnected 50k UST1 → LUNC looks like a **quote**, not a trade; connect with empty UST1 → Guide → `/ust1` with a **legal** deposit amount (≤ 1,000 UST1 notional / remaining 24h). - Same wallet with enough UST1 for a **small** funded swap: no acquire dialog; existing slippage confirm still works. - `make verify-issue-506` still green. New `make verify-issue-<iid>` covers helper + Swap/Trade/Ust1 tests listed above. - Code review: no AMM-as-mint copy; no `git commit --no-verify`; no secrets in fixtures. ## Related - [#293](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/293) Expert Mode / expected slippage - [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/489) retail copy - [#506](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/506) `/ust1` window UI - [#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508) U1 secondary AMM - [#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497) 5% default protection - [#134](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/134) max spread UX
PlasticDigits commented 2026-08-27 00:17:52 +00:00 (Migrated from gitlab.com)

marked as related to #293

marked as related to #293
PlasticDigits commented 2026-08-27 00:17:52 +00:00 (Migrated from gitlab.com)

marked as related to #489

marked as related to #489
PlasticDigits commented 2026-08-27 00:17:53 +00:00 (Migrated from gitlab.com)

marked as related to #506

marked as related to #506
PlasticDigits commented 2026-08-27 00:17:53 +00:00 (Migrated from gitlab.com)

marked as related to #508

marked as related to #508
PlasticDigits commented 2026-08-27 00:17:54 +00:00 (Migrated from gitlab.com)

marked as related to #497

marked as related to #497
PlasticDigits commented 2026-08-27 00:17:55 +00:00 (Migrated from gitlab.com)

marked as related to #134

marked as related to #134
PlasticDigits commented 2026-08-27 00:44:03 +00:00 (Migrated from gitlab.com)

mentioned in commit 88cc86f54e

mentioned in commit 88cc86f54e0baf97ba9b04b4db5600c063153474
PlasticDigits commented 2026-08-27 00:44:43 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1184

mentioned in merge request !1184
PlasticDigits commented 2026-08-27 00:45:02 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1185

mentioned in merge request !1185
PlasticDigits commented 2026-08-27 04:54:44 +00:00 (Migrated from gitlab.com)

mentioned in commit 36d64528b9

mentioned in commit 36d64528b934518688d19c6784d27257e1d70c5e
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-27 04:54:45 +00:00
PlasticDigits commented 2026-08-27 05:29:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #686

mentioned in issue #686
PlasticDigits commented 2026-08-27 05:29:54 +00:00 (Migrated from gitlab.com)

marked as related to #686

marked as related to #686
PlasticDigits commented 2026-08-27 05:30:25 +00:00 (Migrated from gitlab.com)

Merged via !1185 (36d64528) onto main. SwapPage/TradeMarketOrderPanel auto-merged with !1182 mixed-hop gas.

make verify-issue-678 PASS (7/7). Manual leftover: disconnected oversized UST1 quote looks like Quote only; Get UST1 lands on /ust1 with a legal deposit. LocalTerra e2e not run (chain down). Do not reopen unless I595/U1 acquire path is wrong. Post-merge ops: #686.

Merged via !1185 (`36d64528`) onto `main`. SwapPage/TradeMarketOrderPanel auto-merged with !1182 mixed-hop gas. `make verify-issue-678` **PASS** (7/7). Manual leftover: disconnected oversized UST1 quote looks like **Quote only**; **Get UST1** lands on `/ust1` with a legal deposit. LocalTerra e2e not run (chain down). Do not reopen unless I595/U1 acquire path is wrong. Post-merge ops: #686.
PlasticDigits commented 2026-08-29 04:53:39 +00:00 (Migrated from gitlab.com)

Post-merge sanity (!1185 already on main)

Product claims PASS on current main. make verify-issue-678 → 7/7. Shared helper on Swap + Trade market; quote-only / Min received / confirm-again gating; UST1 Guide + inverse deposit clamp; /ust1 prefill safety; 5/30/99 Expert Mode unchanged; U1 Guide is /ust1.

Leftover (ops only, no new issue): manual Coolify/disconnected 50k UST1 quote-only smoke + optional LocalTerra Guide→/ust1 e2e were never run (noted at close). Stack leftover #686 already closed the !1177–!1186 verify including #678.

Do not reopen unless A678/U1 regresses.

## Post-merge sanity (!1185 already on main) Product claims **PASS** on current `main`. `make verify-issue-678` → 7/7. Shared helper on Swap + Trade market; quote-only / Min received / confirm-again gating; UST1 Guide + inverse deposit clamp; `/ust1` prefill safety; 5/30/99 Expert Mode unchanged; U1 Guide is `/ust1`. Leftover (ops only, no new issue): manual Coolify/disconnected 50k UST1 quote-only smoke + optional LocalTerra Guide→`/ust1` e2e were never run (noted at close). Stack leftover #686 already closed the !1177–!1186 verify including #678. Do not reopen unless A678/U1 regresses.
PlasticDigits commented 2026-08-29 04:56:14 +00:00 (Migrated from gitlab.com)

mentioned in issue #701

mentioned in issue #701
PlasticDigits commented 2026-08-31 04:56:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #711

mentioned in issue #711
PlasticDigits commented 2026-08-31 04:56:53 +00:00 (Migrated from gitlab.com)

marked as related to #711

marked as related to #711
PlasticDigits commented 2026-08-31 05:36:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #713

mentioned in issue #713
PlasticDigits commented 2026-08-31 11:16:21 +00:00 (Migrated from gitlab.com)

mentioned in commit d2ddde6fb1

mentioned in commit d2ddde6fb159e53d39da48173c83ada48e29d77e
PlasticDigits commented 2026-08-31 11:18:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #714

mentioned in issue #714
PlasticDigits commented 2026-08-31 16:08:50 +00:00 (Migrated from gitlab.com)

mentioned in issue #715

mentioned in issue #715
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#678
No description provided.