Frontend: uniform one-click Max with gas reserve #213

Closed
opened 2026-05-29 03:15:17 +00:00 by PlasticDigits · 12 comments
PlasticDigits commented 2026-05-29 03:15:17 +00:00 (Migrated from gitlab.com)

Summary

Unify retail Max (and optional 50%) balance actions across all human amount fields, and make Max subtract a gas reserve when the spend asset is native uluna (or when the user opts into native wrap on /pool) so one-click fill does not strand the wallet or fail broadcast for lack of LUNC.

Tracked in gap inventory: gaps/GAP_1780023683.md — One-click “max” with gas reserve (Partial).


Current codebase

Where “Max” exists today

Surface File(s) Behavior
Swap You Pay frontend-dapp/src/pages/SwapPage.tsx Inline Max sets fromRawAmount(balance, decimals) — 100% of wallet balance; no gas reserve for uluna / native pay.
Trade limit escrow LimitOrderEscrowAmountField.tsx → TradeOrderTicket.tsx, LimitOrdersPage.tsx Max → parent onLimitAmountMax; useLimitOrderForm tracks escrowAmountSource: 'max' and re-applies on Bid/Ask switch when balance updates.
Trade market pay TradeMarketOrderPanel.tsx Reuses LimitOrderEscrowAmountField with onMax={setMarketAmountHuman} — full CW20 balance only.
Pool add liquidity PoolPage.tsx Duplicated inline 50% / Max for assets A and B (not shared component).
Swap book leg (Settings advanced) SwapPage.tsx No Max control on bookInputHuman.
Trade book leg override TradeMarketOrderPanel.tsx No Max on hybrid book override field.

These gates block submit when bank uluna is too low for multi-tx CW20 paths; they do not adjust Max amounts:

  • Limit place: limitOrderNativeGasBalanceGate.ts + estimateLimitOrderPlaceSequenceUlunaFeesTotal() — GitLab #132
  • Market swap (CW20): evaluateMarketSwapNativeGasPlaceGate + estimateMarketPairSwapSequenceUlunaFeesTotal() — trade ticket
  • Pool CW20/CW20 provide: provideLiquidityNativeGasBalanceGate.ts + estimateProvideLiquidityCw20SequenceUlunaFeesTotal() — GitLab #147

Fee math is centralized in transactions.ts and terraGas.ts (estimateFeeUlunaAmountForGasLimit, getGasLimitForTx). Gas constants and broadcast invariants are documented in docs/frontend.md § Terra Classic gas limits.

Native swap / wrap paths

  • Native-input swaps use executeNativeSwap in router.ts (coins attached to wrap_deposit or CW20 send → router). Max on native pay must leave enough uluna for the same-tx (or multi-msg) fee envelope, not only for separate allowance txs.
  • Pool Use native … (auto-wrap) toggles spend bank uluna while gas may still require a separate LUNC balance for CW20 allowance sequences.

Why this is needed

  1. UX inconsistency — Swap implements Max inline; trade/limit use LimitOrderEscrowAmountField; pool duplicates 50%/Max. Behavior and disabled states drift (e.g. pool disables Max at balance 0, swap does not identically).
  2. Native “max” foot-gun — Setting pay amount to full uluna often causes insufficient funds or sub-minimum fee failures because LUNC must also pay Fee.amount in the same (or subsequent) transaction(s). Users expect CEX-style Max to be spendable after gas.
  3. False confidence on CW20 Max — User can Max CW20 escrow while LUNC is below the 2× or 3× fee floor; gates block submit but only after Max, wasting a click. Optional hint when Max + low LUNC is acceptable product scope; native reserve is the critical fix.
  4. Gap closure — Product gap explicitly calls for uniform one-click Max with gas reserve; native gas gates alone are insufficient.

Constraints and guardrails

  1. Single source of truth for fee envelopes — Max reserve must derive from existing estimate*UlunaFeesTotal() / getGasLimitForTx() helpers in transactions.ts / terraGas.ts. Do not duplicate magic uluna constants in UI.
  2. BigInt math — Compute max spendable in raw micro-units, then fromRawAmount; never float subtraction on LUNC.
  3. Conservative when balance/fee context loading — Disable Max (same as today) while balance query is loading/error; do not set a partial amount from stale data.
  4. CW20 escrow default — When pay asset is not native uluna, Max remains full CW20 balance (gas is separate LUNC). Do not subtract CW20 for LUNC fees.
  5. Native pay / wrap — When pay asset is uluna or pool native-wrap is enabled for that side, Max = balanceRaw - reserveUluna (floored at 0).
  6. Reserve scope per surface — Pass an explicit MaxAmountContext enum (e.g. swap_native, swap_cw20, limit_place, market_swap, provide_liquidity_native_side, provide_liquidity_cw20) so each screen uses the correct fee envelope (1-tx native swap vs 2-tx CW20 vs 3-tx provide).
  7. Decimal draft rules — Max output must satisfy isDecimalAmountDraft / existing input validators (#169).
  8. Limit max mode — Preserve useLimitOrderForm escrowAmountSource: 'max' re-apply on side change; re-apply must use the new shared compute function so gas reserve stays correct when escrow token changes.
  9. No new broadcast paths — #127: all txs still via broadcastTerraExecuteContracts.
  10. Docs — Update docs/frontend.md with a short § “Max amount / gas reserve” linking to the util and surfaces.

Relevant files

Area Path
Shared UI (extend or supersede) frontend-dapp/src/components/trade/LimitOrderEscrowAmountField.tsx
Swap frontend-dapp/src/pages/SwapPage.tsx
Pool frontend-dapp/src/pages/PoolPage.tsx, frontend-dapp/src/pages/PoolPage.test.tsx
Trade ticket frontend-dapp/src/components/trade/TradeOrderTicket.tsx, TradeMarketOrderPanel.tsx
Limits page frontend-dapp/src/pages/LimitOrdersPage.tsx
Form state frontend-dapp/src/hooks/useLimitOrderForm.ts
Fee estimates frontend-dapp/src/services/terraclassic/transactions.ts, terraGas.ts
Native gas gates limitOrderNativeGasBalanceGate.ts, provideLiquidityNativeGasBalanceGate.ts
Formatting frontend-dapp/src/utils/formatAmount.ts
Native balance frontend-dapp/src/hooks/useNativeUlunaBalance.ts
Router / native swap frontend-dapp/src/services/terraclassic/router.ts
Product gap gaps/GAP_1780023683.md
Agent gas playbook skills/AGENTS_TERRACLASSIC_GAS.md

  1. Add computeMaxSpendableHumanAmount (name TBD) in e.g. frontend-dapp/src/utils/maxSpendableAmount.ts:
    • Inputs: balanceRaw, decimals, assetIsNativeUluna, context: MaxAmountContext, optional nativeUlunaBalance for cross-check hints.
    • For native: reserveRaw = feeEnvelopeForContext (from transactions.ts); optional small safety margin only if already used elsewhere (prefer none unless repro requires).
    • Return { human, cappedByGas: boolean, reserveUluna }.
  2. Extract AmountBalanceActions (Balance row + 50% optional + Max) used by LimitOrderEscrowAmountField, SwapPage, and PoolPage — single styling, disabled rules, sounds.playButtonPress().
  3. Wire each surface with the correct MaxAmountContext and pass native balance query when context needs reserve.
  4. Swap/trade book leg fields: add Max only if product wants parity (recommended: yes, capped to pay amount / balance).
  5. Unit tests on computeMaxSpendableHumanAmount for all contexts; component tests for disabled/loading; extend PoolPage.test.tsx / swap tests for native Max cap.

Acceptance criteria

  • One shared component or field wrapper renders balance + Max (and 50% where present on pool) with identical copy, colors, and disabled rules across swap, pool, trade market, trade limit, and /limits.
  • Max on native uluna pay (swap, native-side pool add with wrap, native swap path) never sets an amount greater than balance - feeEnvelopeForThatAction.
  • Max on CW20 pay sets full CW20 balance (unchanged), with native gas gates still blocking submit when LUNC insufficient.
  • Limit order max mode re-apply after Bid/Ask switch uses the same compute helper.
  • When reserve caps Max to 0, Max is disabled (or no-op with clear disabled state) — no negative or invalid drafts.
  • docs/frontend.md documents Max/gas reserve invariants.
  • Vitest coverage for pure compute function ≥ all MaxAmountContext variants.

Test plan — functional paths

# Path Steps Expected
1 CW20 swap Max Wallet with CW20 + ample LUNC → Swap → Max Full CW20 human amount; submit succeeds if gates pass
2 Native swap Max Wallet with only practical LUNC → Swap native pay → Max Amount < full balance; tx fee payable; no bank insufficient
3 CW20 limit Max /trade or /limits limit tab → Max Full escrow CW20; LUNC gate unchanged
4 Limit max re-apply Max → switch Bid/Ask Amount updates to new escrow max via shared helper
5 Market Max Trade market tab → Max Same as limit field component
6 Pool CW20 Max CW20/CW20 pair → Max A/B Full balances; 50% still half
7 Pool native wrap Max Enable native wrap → Max uluna amount leaves reserve for provide sequence fees
8 Low LUNC + CW20 Max Max CW20 with LUNC below #132/#147 floor Submit disabled; message from existing gate (no regression)
9 Loading balance Max while balance loading Button disabled; no amount set
10 Error balance Balance query error Max disabled; — balance
11 Hybrid book leg Max (if in scope) Settings book leg / trade override → Max ≤ pay amount and ≤ balance
12 LocalTerra + Station Manual on LocalTerra: native Max then swap Signs with fee ≥ effectiveGasPriceUluna() × gas limit (#127)

Automated: npm test in frontend-dapp for new unit/component tests; optional Playwright smoke on swap Max if E2E wallet funded.


Test plan — attack / abuse vectors

# Vector Mitigation to verify
A1 UI bypass — Manually type amount > balance after Max Escrow gate / insufficient balance still blocks submit (limitOrderEscrowBalanceGate, pool “Exceeds wallet balance”)
A2 Stale balance — Max then balance drops before sign On-chain fails safely; no extra allowance broadcast if gates re-run pre-submit
A3 Reserve understatement — Maliciously low reserve constant Use only transactions.ts / terraGas.ts estimates; regression test compares reserve ≥ estimateFeeUlunaAmountForGasLimit for context
A4 Reserve overstatement — Max always 0 Cap only native; CW20 unaffected; user can still type manual amount
A5 Precision / rounding — Max human rounds up past balance Raw BigInt cap before fromRawAmount; submit raw ≤ balance
A6 Cross-asset confusion — Max on CW20 subtracts LUNC from CW20 amount assetIsNativeUluna guard in unit tests
A7 Re-apply loop — Side switch + max mode infinite updates Effect deps unchanged; only updates when balance/decimals/context change
A8 Locale / invalid draft — Max produces scientific notation Output passes isDecimalAmountDraft

Verification criteria

  • All acceptance criteria checked.
  • frontend-dapp Vitest green; new tests named for GitLab issue IID.
  • Manual native Max on LocalTerra succeeds for swap and does not for “full balance” control (before/after comparison).
  • No duplicate Max UI implementations remain in SwapPage.tsx / PoolPage.tsx (grep for fromRawAmount(balance in Max handlers → single helper).
  • docs/frontend.md PR section or anchor updated; skills/AGENTS_TERRACLASSIC_GAS.md cross-links if reserve touches gas docs.
  • Gap row in gaps/GAP_1780023683.md updated to Done or Improved when merged.

Labels / metadata

  • Labels: frontend, ux
  • Owner type: frontend
  • Priority: P2
  • Dependencies: Builds on #132, #147, #127, #169 (decimal inputs); does not change contracts or indexer.
## Summary Unify retail **Max** (and optional **50%**) balance actions across all human amount fields, and make **Max** subtract a **gas reserve** when the spend asset is native **uluna** (or when the user opts into native wrap on `/pool`) so one-click fill does not strand the wallet or fail broadcast for lack of LUNC. Tracked in gap inventory: `gaps/GAP_1780023683.md` — *One-click “max” with gas reserve* (**Partial**). --- ## Current codebase ### Where “Max” exists today | Surface | File(s) | Behavior | |---------|---------|----------| | Swap **You Pay** | `frontend-dapp/src/pages/SwapPage.tsx` | Inline **Max** sets `fromRawAmount(balance, decimals)` — **100%** of wallet balance; no gas reserve for `uluna` / native pay. | | Trade **limit** escrow | `LimitOrderEscrowAmountField.tsx` → `TradeOrderTicket.tsx`, `LimitOrdersPage.tsx` | **Max** → parent `onLimitAmountMax`; `useLimitOrderForm` tracks `escrowAmountSource: 'max'` and **re-applies** on Bid/Ask switch when balance updates. | | Trade **market** pay | `TradeMarketOrderPanel.tsx` | Reuses `LimitOrderEscrowAmountField` with `onMax={setMarketAmountHuman}` — full CW20 balance only. | | Pool **add liquidity** | `PoolPage.tsx` | Duplicated inline **50%** / **Max** for assets A and B (not shared component). | | Swap **book leg** (Settings advanced) | `SwapPage.tsx` | No **Max** control on `bookInputHuman`. | | Trade **book leg override** | `TradeMarketOrderPanel.tsx` | No **Max** on hybrid book override field. | ### Related gas preflight (submit-time, not Max-time) These gates block submit when **bank uluna** is too low for **multi-tx CW20** paths; they do **not** adjust Max amounts: - Limit place: `limitOrderNativeGasBalanceGate.ts` + `estimateLimitOrderPlaceSequenceUlunaFeesTotal()` — [GitLab #132](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/132) - Market swap (CW20): `evaluateMarketSwapNativeGasPlaceGate` + `estimateMarketPairSwapSequenceUlunaFeesTotal()` — trade ticket - Pool CW20/CW20 provide: `provideLiquidityNativeGasBalanceGate.ts` + `estimateProvideLiquidityCw20SequenceUlunaFeesTotal()` — [GitLab #147](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/147) Fee math is centralized in `transactions.ts` and `terraGas.ts` (`estimateFeeUlunaAmountForGasLimit`, `getGasLimitForTx`). Gas constants and broadcast invariants are documented in [`docs/frontend.md` § Terra Classic gas limits](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md#terra-classic-gas-limits). ### Native swap / wrap paths - Native-input swaps use `executeNativeSwap` in `router.ts` (coins attached to `wrap_deposit` or CW20 `send` → router). **Max** on native pay must leave enough **uluna** for the **same-tx** (or multi-msg) fee envelope, not only for separate allowance txs. - Pool **Use native … (auto-wrap)** toggles spend bank `uluna` while gas may still require a separate LUNC balance for CW20 allowance sequences. --- ## Why this is needed 1. **UX inconsistency** — Swap implements Max inline; trade/limit use `LimitOrderEscrowAmountField`; pool duplicates 50%/Max. Behavior and disabled states drift (e.g. pool disables Max at balance `0`, swap does not identically). 2. **Native “max” foot-gun** — Setting pay amount to **full uluna** often causes `insufficient funds` or sub-minimum fee failures because LUNC must also pay `Fee.amount` in the same (or subsequent) transaction(s). Users expect CEX-style Max to be **spendable after gas**. 3. **False confidence on CW20 Max** — User can Max CW20 escrow while LUNC is below the **2× or 3×** fee floor; gates block submit but only **after** Max, wasting a click. Optional hint when Max + low LUNC is acceptable product scope; **native reserve** is the critical fix. 4. **Gap closure** — Product gap explicitly calls for uniform one-click Max with gas reserve; native gas gates alone are insufficient. --- ## Constraints and guardrails 1. **Single source of truth for fee envelopes** — Max reserve must derive from existing `estimate*UlunaFeesTotal()` / `getGasLimitForTx()` helpers in `transactions.ts` / `terraGas.ts`. Do not duplicate magic uluna constants in UI. 2. **BigInt math** — Compute max spendable in **raw micro-units**, then `fromRawAmount`; never float subtraction on LUNC. 3. **Conservative when balance/fee context loading** — Disable Max (same as today) while balance query is loading/error; do not set a partial amount from stale data. 4. **CW20 escrow default** — When pay asset is **not** native `uluna`, Max remains **full CW20 balance** (gas is separate LUNC). Do not subtract CW20 for LUNC fees. 5. **Native pay / wrap** — When pay asset is `uluna` or pool native-wrap is enabled for that side, Max = `balanceRaw - reserveUluna` (floored at `0`). 6. **Reserve scope per surface** — Pass an explicit `MaxAmountContext` enum (e.g. `swap_native`, `swap_cw20`, `limit_place`, `market_swap`, `provide_liquidity_native_side`, `provide_liquidity_cw20`) so each screen uses the correct fee envelope (1-tx native swap vs 2-tx CW20 vs 3-tx provide). 7. **Decimal draft rules** — Max output must satisfy [`isDecimalAmountDraft`](frontend-dapp/src/utils/decimalAmountInput.ts) / existing input validators ([#169](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/169)). 8. **Limit `max` mode** — Preserve `useLimitOrderForm` `escrowAmountSource: 'max'` re-apply on side change; re-apply must use the **new** shared compute function so gas reserve stays correct when escrow token changes. 9. **No new broadcast paths** — [#127](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/127): all txs still via `broadcastTerraExecuteContracts`. 10. **Docs** — Update [`docs/frontend.md`](docs/frontend.md) with a short § “Max amount / gas reserve” linking to the util and surfaces. --- ## Relevant files | Area | Path | |------|------| | Shared UI (extend or supersede) | `frontend-dapp/src/components/trade/LimitOrderEscrowAmountField.tsx` | | Swap | `frontend-dapp/src/pages/SwapPage.tsx` | | Pool | `frontend-dapp/src/pages/PoolPage.tsx`, `frontend-dapp/src/pages/PoolPage.test.tsx` | | Trade ticket | `frontend-dapp/src/components/trade/TradeOrderTicket.tsx`, `TradeMarketOrderPanel.tsx` | | Limits page | `frontend-dapp/src/pages/LimitOrdersPage.tsx` | | Form state | `frontend-dapp/src/hooks/useLimitOrderForm.ts` | | Fee estimates | `frontend-dapp/src/services/terraclassic/transactions.ts`, `terraGas.ts` | | Native gas gates | `limitOrderNativeGasBalanceGate.ts`, `provideLiquidityNativeGasBalanceGate.ts` | | Formatting | `frontend-dapp/src/utils/formatAmount.ts` | | Native balance | `frontend-dapp/src/hooks/useNativeUlunaBalance.ts` | | Router / native swap | `frontend-dapp/src/services/terraclassic/router.ts` | | Product gap | `gaps/GAP_1780023683.md` | | Agent gas playbook | `skills/AGENTS_TERRACLASSIC_GAS.md` | --- ## Recommended direction 1. Add **`computeMaxSpendableHumanAmount`** (name TBD) in e.g. `frontend-dapp/src/utils/maxSpendableAmount.ts`: - Inputs: `balanceRaw`, `decimals`, `assetIsNativeUluna`, `context: MaxAmountContext`, optional `nativeUlunaBalance` for cross-check hints. - For native: `reserveRaw = feeEnvelopeForContext` (from `transactions.ts`); optional small safety margin only if already used elsewhere (prefer none unless repro requires). - Return `{ human, cappedByGas: boolean, reserveUluna }`. 2. Extract **`AmountBalanceActions`** (Balance row + **50%** optional + **Max**) used by `LimitOrderEscrowAmountField`, `SwapPage`, and `PoolPage` — single styling, disabled rules, `sounds.playButtonPress()`. 3. Wire each surface with the correct `MaxAmountContext` and pass native balance query when context needs reserve. 4. Swap/trade **book leg** fields: add Max only if product wants parity (recommended: yes, capped to pay amount / balance). 5. Unit tests on `computeMaxSpendableHumanAmount` for all contexts; component tests for disabled/loading; extend `PoolPage.test.tsx` / swap tests for native Max cap. --- ## Acceptance criteria - [ ] One shared component or field wrapper renders balance + **Max** (and **50%** where present on pool) with identical copy, colors, and disabled rules across swap, pool, trade market, trade limit, and `/limits`. - [ ] **Max** on native `uluna` pay (swap, native-side pool add with wrap, native swap path) never sets an amount greater than `balance - feeEnvelopeForThatAction`. - [ ] **Max** on CW20 pay sets full CW20 balance (unchanged), with native gas gates still blocking submit when LUNC insufficient. - [ ] Limit order **max mode** re-apply after Bid/Ask switch uses the same compute helper. - [ ] When reserve caps Max to `0`, **Max** is disabled (or no-op with clear disabled state) — no negative or invalid drafts. - [ ] `docs/frontend.md` documents Max/gas reserve invariants. - [ ] Vitest coverage for pure compute function ≥ all `MaxAmountContext` variants. --- ## Test plan — functional paths | # | Path | Steps | Expected | |---|------|-------|----------| | 1 | CW20 swap Max | Wallet with CW20 + ample LUNC → Swap → Max | Full CW20 human amount; submit succeeds if gates pass | | 2 | Native swap Max | Wallet with only practical LUNC → Swap native pay → Max | Amount &lt; full balance; tx fee payable; no bank insufficient | | 3 | CW20 limit Max | `/trade` or `/limits` limit tab → Max | Full escrow CW20; LUNC gate unchanged | | 4 | Limit max re-apply | Max → switch Bid/Ask | Amount updates to new escrow max via shared helper | | 5 | Market Max | Trade market tab → Max | Same as limit field component | | 6 | Pool CW20 Max | CW20/CW20 pair → Max A/B | Full balances; 50% still half | | 7 | Pool native wrap Max | Enable native wrap → Max | uluna amount leaves reserve for provide sequence fees | | 8 | Low LUNC + CW20 Max | Max CW20 with LUNC below #132/#147 floor | Submit disabled; message from existing gate (no regression) | | 9 | Loading balance | Max while balance loading | Button disabled; no amount set | | 10 | Error balance | Balance query error | Max disabled; `—` balance | | 11 | Hybrid book leg Max (if in scope) | Settings book leg / trade override → Max | ≤ pay amount and ≤ balance | | 12 | LocalTerra + Station | Manual on LocalTerra: native Max then swap | Signs with fee ≥ `effectiveGasPriceUluna()` × gas limit ([#127](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/127)) | **Automated:** `npm test` in `frontend-dapp` for new unit/component tests; optional Playwright smoke on swap Max if E2E wallet funded. --- ## Test plan — attack / abuse vectors | # | Vector | Mitigation to verify | |---|--------|---------------------| | A1 | **UI bypass** — Manually type amount &gt; balance after Max | Escrow gate / insufficient balance still blocks submit (`limitOrderEscrowBalanceGate`, pool “Exceeds wallet balance”) | | A2 | **Stale balance** — Max then balance drops before sign | On-chain fails safely; no extra allowance broadcast if gates re-run pre-submit | | A3 | **Reserve understatement** — Maliciously low reserve constant | Use only `transactions.ts` / `terraGas.ts` estimates; regression test compares reserve ≥ `estimateFeeUlunaAmountForGasLimit` for context | | A4 | **Reserve overstatement** — Max always 0 | Cap only native; CW20 unaffected; user can still type manual amount | | A5 | **Precision / rounding** — Max human rounds up past balance | Raw BigInt cap before `fromRawAmount`; submit raw ≤ balance | | A6 | **Cross-asset confusion** — Max on CW20 subtracts LUNC from CW20 amount | `assetIsNativeUluna` guard in unit tests | | A7 | **Re-apply loop** — Side switch + max mode infinite updates | Effect deps unchanged; only updates when balance/decimals/context change | | A8 | **Locale / invalid draft** — Max produces scientific notation | Output passes `isDecimalAmountDraft` | --- ## Verification criteria - [ ] All acceptance criteria checked. - [ ] `frontend-dapp` Vitest green; new tests named for GitLab issue IID. - [ ] Manual native Max on LocalTerra succeeds for swap and does not for “full balance” control (before/after comparison). - [ ] No duplicate Max UI implementations remain in `SwapPage.tsx` / `PoolPage.tsx` (grep for `fromRawAmount(balance` in Max handlers → single helper). - [ ] `docs/frontend.md` PR section or anchor updated; `skills/AGENTS_TERRACLASSIC_GAS.md` cross-links if reserve touches gas docs. - [ ] Gap row in `gaps/GAP_1780023683.md` updated to **Done** or **Improved** when merged. --- ## Labels / metadata - **Labels:** `frontend`, `ux` - **Owner type:** frontend - **Priority:** P2 - **Dependencies:** Builds on #132, #147, #127, #169 (decimal inputs); does not change contracts or indexer.
PlasticDigits commented 2026-05-29 03:31:22 +00:00 (Migrated from gitlab.com)

Implementation summary (merged to main @ 79e66e5)

Implemented GitLab #213: uniform one-click Max (and pool 50%) with native uluna gas reserve.

What changed

  • computeMaxSpendableHumanAmount + MaxAmountContext in frontend-dapp/src/utils/maxSpendableAmount.ts — single source for Max math (BigInt reserve, then fromRawAmount).
  • AmountBalanceActions shared UI component — balance row + optional 50% + Max (Swap, Pool, limit/market tickets, /limits).
  • Fee reserve helpers in transactions.ts: estimateNativeSwapUlunaFeesTotal, estimateProvideLiquidityNativeWrapUlunaFeesTotal (aligned with terraGas.ts / existing sequence estimates).
  • Wired surfaces: Swap (pay + hybrid book leg Max), Pool (A/B with native-wrap reserve), Trade limit + market, /limits (max-mode re-apply via useLimitEscrowMaxReapply).
  • Docs: docs/frontend.md § Max amount / gas reserve, skills/AGENTS_TERRACLASSIC_GAS.md, gap row Done in gaps/GAP_1780023683.md.
  • Tests: maxSpendableAmount.test.ts (all contexts), extended transactions.test.ts, updated PoolPage.test.tsx.

Verification checklist

  • CW20 swap Max — full CW20 balance; submit succeeds when LUNC gates pass
  • Native swap Max — amount < full LUNC balance; tx signs without bank insufficient funds
  • Limit Max — full escrow CW20; Bid/Ask switch re-applies via shared helper
  • Market Max — same shared field component as limit
  • Pool CW20 Max — full balances; 50% still half
  • Pool native wrap Max — uluna Max leaves fee reserve for combined multi-msg provide
  • Low LUNC + CW20 Max — submit still blocked by #132/#147 gates (no regression)
  • Loading/error balance — Max disabled; no partial amount set
  • Hybrid book leg Max — ≤ pay amount and ≤ balance (Swap Settings + Trade market override)
  • Vitest — npm run test:run in frontend-dapp green for maxSpendableAmount.test.ts
  • Manual LocalTerra — native Max then swap succeeds; typing full balance manually still fails safely

@brouie — please verify on LocalTerra when you have a moment. Leaving this issue open until sign-off.

## Implementation summary (merged to `main` @ 79e66e5) Implemented [GitLab #213](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/213): uniform one-click **Max** (and pool **50%**) with native **uluna** gas reserve. ### What changed - **`computeMaxSpendableHumanAmount`** + **`MaxAmountContext`** in `frontend-dapp/src/utils/maxSpendableAmount.ts` — single source for Max math (BigInt reserve, then `fromRawAmount`). - **`AmountBalanceActions`** shared UI component — balance row + optional **50%** + **Max** (Swap, Pool, limit/market tickets, `/limits`). - Fee reserve helpers in **`transactions.ts`**: `estimateNativeSwapUlunaFeesTotal`, `estimateProvideLiquidityNativeWrapUlunaFeesTotal` (aligned with `terraGas.ts` / existing sequence estimates). - Wired surfaces: **Swap** (pay + hybrid book leg Max), **Pool** (A/B with native-wrap reserve), **Trade** limit + market, **`/limits`** (max-mode re-apply via `useLimitEscrowMaxReapply`). - **Docs**: [`docs/frontend.md` § Max amount / gas reserve](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md#max-amount-gas-reserve), [`skills/AGENTS_TERRACLASSIC_GAS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_TERRACLASSIC_GAS.md), gap row **Done** in `gaps/GAP_1780023683.md`. - **Tests**: `maxSpendableAmount.test.ts` (all contexts), extended `transactions.test.ts`, updated `PoolPage.test.tsx`. ### Verification checklist - [ ] **CW20 swap Max** — full CW20 balance; submit succeeds when LUNC gates pass - [ ] **Native swap Max** — amount &lt; full LUNC balance; tx signs without bank insufficient funds - [ ] **Limit Max** — full escrow CW20; Bid/Ask switch re-applies via shared helper - [ ] **Market Max** — same shared field component as limit - [ ] **Pool CW20 Max** — full balances; **50%** still half - [ ] **Pool native wrap Max** — uluna Max leaves fee reserve for combined multi-msg provide - [ ] **Low LUNC + CW20 Max** — submit still blocked by #132/#147 gates (no regression) - [ ] **Loading/error balance** — Max disabled; no partial amount set - [ ] **Hybrid book leg Max** — ≤ pay amount and ≤ balance (Swap Settings + Trade market override) - [ ] **Vitest** — `npm run test:run` in `frontend-dapp` green for `maxSpendableAmount.test.ts` - [ ] **Manual LocalTerra** — native Max then swap succeeds; typing full balance manually still fails safely @brouie — please verify on LocalTerra when you have a moment. Leaving this issue **open** until sign-off.
PlasticDigits commented 2026-05-29 03:31:26 +00:00 (Migrated from gitlab.com)

mentioned in commit 0a1e90af7d

mentioned in commit 0a1e90af7d5616a14131b69f127074c90ad08dca
PlasticDigits commented 2026-05-29 03:31:26 +00:00 (Migrated from gitlab.com)

mentioned in commit 79e66e522c

mentioned in commit 79e66e522c085d8d9f20ceeddf31ae2e8ef182e5
PlasticDigits commented 2026-05-29 03:35:05 +00:00 (Migrated from gitlab.com)

mentioned in commit e02d991ce9

mentioned in commit e02d991ce9169189255107d8965ec77f5e2f0c85
PlasticDigits commented 2026-05-29 12:17:09 +00:00 (Migrated from gitlab.com)

Verification complete (agent, verify/issue-213 worktree @ 308a04a)

Re-verified GitLab #213 on LocalTerra (127.0.0.1:26657, LCD 1317, indexer 3001) with frontend-dapp Vitest and browser smoke on http://127.0.0.1:5174 (simulated dev wallet terra1x46…20k38v).

What was checked

  • Acceptance criteria: shared AmountBalanceActions on Swap / Pool / trade limit+market / /limits; computeMaxSpendableHumanAmount + MaxAmountContext; native uluna reserve from transactions.ts / terraGas.ts; CW20 Max full balance; limit max re-apply via useLimitEscrowMaxReapply; docs (docs/frontend.md § Max amount / gas reserve), skills/AGENTS_TERRACLASSIC_GAS.md, gap GAP_1780023683.md → Done.
  • Automated: npm test -- --run in frontend-dapp — 114 files / 706 tests passed (includes maxSpendableAmount.test.ts GitLab #213, transactions.test.ts reserve helpers).
  • Grep: no fromRawAmount(balance Max handlers in page components.
  • Browser: CW20 (EMBER) Max → 700376.545989 (full CW20). Native LUNC Max → 69789744.486 vs bank 69789800.286250 LUNC — ~55.8M uluna reserve left for fees (not full balance).

Checklist for humans (optional re-run)

  • cd frontend-dapp && npm test -- --run
  • Swap: CW20 pay → Max fills full token balance; submit gated if LUNC low (#132 path unchanged)
  • Swap: native LUNC pay → Max < wallet balance; swap signs without bank insufficient funds
  • /trade limit + market and /limits: Max + Bid/Ask re-apply after Max
  • /pool: 50% / Max on A/B; native wrap Max leaves LUNC reserve
  • Station extension on LocalTerra: native Max then swap (fee ≥ effectiveGasPriceUluna() × gas limit, #127)

No code changes required on this pass; main already contains the implementation.

Closing as verified.

## Verification complete (agent, `verify/issue-213` worktree @ `308a04a`) Re-verified GitLab #213 on LocalTerra (`127.0.0.1:26657`, LCD `1317`, indexer `3001`) with `frontend-dapp` Vitest and browser smoke on `http://127.0.0.1:5174` (simulated dev wallet `terra1x46…20k38v`). ### What was checked - **Acceptance criteria:** shared `AmountBalanceActions` on Swap / Pool / trade limit+market / `/limits`; `computeMaxSpendableHumanAmount` + `MaxAmountContext`; native `uluna` reserve from `transactions.ts` / `terraGas.ts`; CW20 Max full balance; limit max re-apply via `useLimitEscrowMaxReapply`; docs (`docs/frontend.md` § Max amount / gas reserve), `skills/AGENTS_TERRACLASSIC_GAS.md`, gap `GAP_1780023683.md` → **Done**. - **Automated:** `npm test -- --run` in `frontend-dapp` — **114 files / 706 tests passed** (includes `maxSpendableAmount.test.ts` GitLab #213, `transactions.test.ts` reserve helpers). - **Grep:** no `fromRawAmount(balance` Max handlers in page components. - **Browser:** CW20 (EMBER) **Max** → `700376.545989` (full CW20). Native **LUNC** **Max** → `69789744.486` vs bank `69789800.286250` LUNC — **~55.8M uluna reserve** left for fees (not full balance). ### Checklist for humans (optional re-run) - [ ] `cd frontend-dapp && npm test -- --run` - [ ] Swap: CW20 pay → **Max** fills full token balance; submit gated if LUNC low (#132 path unchanged) - [ ] Swap: native LUNC pay → **Max** < wallet balance; swap signs without bank `insufficient funds` - [ ] `/trade` limit + market and `/limits`: **Max** + Bid/Ask re-apply after **Max** - [ ] `/pool`: **50%** / **Max** on A/B; native wrap **Max** leaves LUNC reserve - [ ] Station extension on LocalTerra: native **Max** then swap (fee ≥ `effectiveGasPriceUluna()` × gas limit, #127) No code changes required on this pass; `main` already contains the implementation. Closing as verified.
PlasticDigits commented 2026-05-29 12:18:59 +00:00 (Migrated from gitlab.com)

Reopening for test plan item 12 only — prior agent verification did not cover Station extension signing on LocalTerra.

@brouie Please verify:

# Path Steps Expected
12 LocalTerra + Station Manual on LocalTerra: native Max on swap pay (LUNC), then submit swap Signs with fee ≥ effectiveGasPriceUluna() × gas limit (#127); no bank insufficient funds from setting pay to full balance

Already verified (agent): shared Max UI, native gas reserve math, Vitest (maxSpendableAmount.test.ts), simulated-wallet browser smoke (CW20 full Max; native Max < balance). See note_3398966349.

Close when: item 12 passes with Station on LocalTerra; all other acceptance criteria remain satisfied on main.

Reopening for **test plan item 12** only — prior agent verification did not cover **Station extension** signing on LocalTerra. @brouie Please verify: | # | Path | Steps | Expected | |---|------|-------|----------| | 12 | LocalTerra + Station | Manual on LocalTerra: native **Max** on swap pay (LUNC), then submit swap | Signs with fee ≥ `effectiveGasPriceUluna()` × gas limit ([#127](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/127)); no bank `insufficient funds` from setting pay to full balance | **Already verified (agent):** shared Max UI, native gas reserve math, Vitest (`maxSpendableAmount.test.ts`), simulated-wallet browser smoke (CW20 full Max; native Max &lt; balance). See [note_3398966349](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/213#note_3398966349). **Close when:** item 12 passes with Station on LocalTerra; all other acceptance criteria remain satisfied on `main`.
PlasticDigits commented 2026-05-30 09:07:15 +00:00 (Migrated from gitlab.com)

Closing as testing on localterra terrastation is not possible, instead should be keplr.

Closing as testing on localterra terrastation is not possible, instead should be keplr.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-30 09:07:17 +00:00
PlasticDigits commented 2026-08-16 07:14:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #531

mentioned in issue #531
PlasticDigits commented 2026-08-16 09:55:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #533

mentioned in issue #533
PlasticDigits commented 2026-08-21 11:29:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #587

mentioned in issue #587
PlasticDigits commented 2026-08-21 11:29:51 +00:00 (Migrated from gitlab.com)

marked as related to #587

marked as related to #587
PlasticDigits commented 2026-08-23 03:05:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #599

mentioned in issue #599
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#213
No description provided.