Skip to content

feat(swap): miles calculator redesign + slippage/gas/estimator hardening#142

Open
passandscore wants to merge 11 commits intomainfrom
miles-flow
Open

feat(swap): miles calculator redesign + slippage/gas/estimator hardening#142
passandscore wants to merge 11 commits intomainfrom
miles-flow

Conversation

@passandscore
Copy link
Copy Markdown
Contributor

Summary

  • Miles calculator: redesigned as a 3-state pill (collapsed badge → "Earn upto N miles" + Enable → "Earn [n] of N miles" + Calculate). Calc only adjusts slippage; user's typed sell/buy amount is untouched. Resets on token switch, swap-input change, calc close, and successful preconfirmation.
  • Estimator math: corrected slippage planner using the forward calc's last observed effective rate so applied targets land within 0–1 mile of the typed value. New reactive maxAchievableMiles matches what the bar will produce on Apply. isBarterValidating gate freezes the bar + max at last-good values during token-switch transitions.
  • Auto-slippage: bumped buffer 0.5%→1.0% to absorb execution-time drift, synchronously reset the shortfall ratchet on token switch, and made slippage > autoBaseline paint the gear amber regardless of mode.
  • Wallet gas display: replaced the 1.95× fudge with the wallet's actual cost formula; ETH-path tx now sets maxPriorityFeePerGas: 0n (FastSwap inclusion is paid by the bidder).
  • Stale-data sanity: computeSurplusEth sanity gate ([0.5×, 2×] of uniswap quote) and useBarterValidation synchronous storedForKey gating + >90% shortfall rejection — eliminates the order-of-magnitude wrong values during token-switch transitions.
  • Tests: 56 new tests, 106 project-wide, all passing. Includes 5k-iteration fuzz on the inverse↔forward miles round-trip.

Test plan

  • Cold load: miles bar renders without flashing TBD; calc opens to "Earn upto N miles".
  • Type a target in the calc, click Calculate (or Enter / blur) → bar matches typed target ±1 mile, slippage gear stays amber.
  • Switch tokens repeatedly via the center button → no order-of-magnitude wrong miles, slippage doesn't get stuck at 50%, calc collapses to its pre-Enable view.
  • Successful swap (preconfirmation) → calc fully collapses to the badge.
  • Wallet popup gas vs. app gas display: within a few cents.
  • ETH-input swap: wallet shows priority fee = 0; permit-input swap: user pays no L1 gas.
  • npm test — all 106 tests pass.

Add milesToAmountOut inverse helper to useEstimatedMiles so a target
miles value resolves to the required buy-token amount in closed form
(no extra RPC). RewardsBadge expands smoothly to the swap interface
width with a single pill that animates max-width and crossfades
between the collapsed badge and a target -> required buy amount
calculator. Typing live-populates the buy field via setEditingSide
"buy" + setAmount; the existing quote pipeline takes over from there
and miles re-derive from the real amountOut, so post-quote drift is
handled naturally.

Also gitignore the .claude scheduled-tasks lock file.
Replace the 1.95× display fudge multiplier with the wallet's actual cost
formula: gasLimit × (baseFee + priorityFee) × ethPrice. ETH-path tx now
populates `maxPriorityFeePerGas: 0n` since FastSwap inclusion is paid by
the bidder via mev-commit, so the user pays no L1 tip.

Confirmation modal `gasCostUsd` uses the un-buffered eth_estimateGas (matches
wallet's "estimated cost" panel, not the max-cost ceiling) plus a 1.20×
display padding for the wallet-side safety margin.

Net: app's gas display lands within a few cents of the wallet popup
instead of being ~2-3× off.
- Bump AUTO_BUMP_BUFFER_PCT from 0.5 to 1.0 to absorb execution-time
  routing drift between Barter validation and bidder fill — the
  previous 0.5% margin was leaving slippage-too-low reverts on small
  swaps.
- Auto slippage = max(autoBase, shortfall + buffer) so even shortfalls
  below autoBase get the safety margin.
- Synchronously reset observedBarterShortfallPct + slippage inside
  handleSwitch BEFORE the 500ms debounce, so the new pair starts from
  a clean ratchet instead of inheriting the previous direction's
  bumped value.
- TransactionSettings gear/pill now amber whenever slippage > the
  auto baseline, regardless of mode — calc-applied custom slippage
  stays yellow instead of flipping to blue. Auto-bumped popup notice
  still gates on auto mode only (per spec).
- Export computeAutoBumpValue / formatSlippage / finalizeSlippage /
  sanitizeInput / new computeAutoSlippage for unit testing.
- New swapResetCount counter incremented in resetFormAfterSuccess so
  downstream consumers (miles calc) can clear their session state on
  preconfirmation.
…ening

Calculator UI:
- Three-state machine: collapsed badge → "Earn upto N miles" + Enable →
  "Earn [n] of N miles" + Calculate. Calc only adjusts slippage, never
  the user's typed sell/buy amount.
- Resets on token switch, swap-input change, calc close, and successful
  preconfirmation (via swapResetCount).
- Input hard-capped at maxAchievableMiles so users can't type a target
  the system can't deliver.

Estimator (use-estimated-miles):
- New milesToSlippage planner uses the forward calc's last observed
  effective rate (lastEffectiveSurplusRateRef) so applied slippage
  produces miles equal to the user's typed target. Math.ceil at 0.01%
  step + 5e-7 floor epsilon → applied target meets typed value
  within 0–1 mile.
- maxAchievableMiles is now a reactive memo using the same
  computeSurplusEth function the forward uses, evaluated at the 50%
  cap — so the calc's max matches what the bar will produce on Apply.
- isBarterValidating gate freezes both estimatedMiles and
  maxAchievableMiles at last-good values during transitions; lastMaxRef
  mirrors the lastMilesRef pattern for the max display.
- DEFAULT_PRIORITY_FEE_WEI initial state so cold load doesn't flash
  TBD while the FastRPC bid-estimate poll completes.
- ETH-path forward gate only requires priorityFee, not baseFee
  (baseFee is unused on that path).

Stale-data sanity gates:
- computeSurplusEth returns null when barterPreGasHuman is outside
  [0.5×, 2×] of the uniswap quote — catches decimals-mismatch from
  stale-pair quotes during token switches.
- useBarterValidation: storedForKey synchronously gates returned values
  on the current inputKey so a stale frame can't expose previous-pair
  state. Drops shortfall measurements > 90% as stale-quote artifacts.

Net: token switches no longer flash order-of-magnitude wrong miles or
get stuck at 50% slippage from a stale ratchet; calculator typed value
lands accurately in the bar after Apply.
Adds 56 new tests (106 total project-wide, all passing).

Coverage:
- computeSurplusEth: stale-data sanity gate (decimals mismatch in both
  directions), token decimals (USDC 6, WBTC 8, DAI 18), fuzz invariants
  (non-negativity, monotonicity in slippage, sanity-gate trigger rate).
- Slippage helpers (computeAutoBumpValue, computeAutoSlippage,
  formatSlippage, finalizeSlippage, sanitizeInput): bounds, monotonicity,
  step alignment, rounding edges + 10k-iteration fuzz.
- Miles math invariants (miles-math.test.ts): forward formula, planner
  inverse, max-miles formula, inverse↔forward round-trip fuzz across
  5,000 random {amount, slippage, lastEffectiveRate, target} triples
  asserting forward(milesToSlippage(target)) ≥ target — the user's
  "miles I apply must be added as is" requirement.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fastprotocolapp Ready Ready Preview, Comment May 2, 2026 2:19pm

Request Review

…resChange

Stale type signature on the prop still referenced `slippageBumped` after
the field was renamed to `requiresChange` — TypeScript build failed on
the type mismatch in SwapForm. Aligning with the canonical interface.
When `maxAchievableMiles === 0` (gas+bid+sweep costs exceed even the 50%
slippage ceiling), the calc previously fell back to the generic "Earn
miles on this swap" copy and disabled the Enable button silently —
giving the user no idea why they couldn't engage.

Now distinguishes three states explicitly:
  - maxMiles == null  → "Earn miles on this swap" (data still loading)
  - maxMiles >  0     → "Earn up to N miles"
  - maxMiles == 0     → "Swap too small to earn miles at current gas"
                        (amber-tinted to flag it as actionable info)

Added a regression test reproducing the scenario from the field: tiny
permit-path swap (~$2) where the 2.5× sweep multiplier on bid+gas costs
dwarfs the 50%-slippage surplus ceiling, so max miles = 0.
…d estimate

- BuyCard: when the miles calc applies slippage, header reads
  "Buy · miles applied" and the USD price line shows
  "≈ $<min-usd> (−<token-delta>)" with a tooltip explaining the cost.
- SwapForm: tracks miles-applied state, clears on slippage drift, token
  switch, or successful swap so the buy card never lies.
- Dashboard MilesCell: re-runs the forward miles calc against on-chain
  surplus/gas instead of preferring the swap-time stash. Sanity-gates
  realized vs. stash to suppress mid-write flicker; tooltip explains
  why the dashboard number may differ from what the user saw at swap time.
- RewardsBadge: copy tweaks (Estimate Miles label, simpler small-swap msg).
Hardens auto-slippage and the miles estimator against bugs where
toggling sell amount via the percentage buttons produced phantom 50%
slippage and inflated miles, plus a batch of miles-calc UX upgrades.

Stability fixes:
- Linear computeAutoBumpValue (no pre-buffer step rounding) so any
  positive shortfall under the buffer no longer stair-steps auto from
  baseline 1.0% to 1.1%. Final value still 0.1%-aligned for display.
- Sync customSlippage to autoSlippage on custom→auto transition so the
  next custom-mode entry doesn't restore a stale value.
- Lower barter sanity gate from 90% → 50% and stop silently swallowing:
  settle with sanityGated=true so amountTooSmall fires and the user
  sees an explicit warning instead of auto silently railing to 50%.
- Defer barter validation while uniswap quote is loading. Without this,
  rapid amount changes fire validation with fresh barter for the new
  size vs stale uniswap output for the prior size — manufacturing a
  40-50% phantom shortfall the only-goes-up ratchet then locks in.
- Clear lastMilesRef + lastEffectiveSurplusRateRef when the swap
  identity changes (typed amount + pair) so prior-amount values don't
  leak into the new amount during the validation window.

Miles calc UX:
- Snake-border accent on the collapsed pill via CSS Motion Path.
- Apply now opens a confirmation overlay (✓/✗ over the same pill)
  before mutating slippage, with concise centered copy.
- "of N miles" label became an outline button that fills the input
  with the max value.
- Calc fully closes (and resets slippage to auto) on any sell-amount
  change — manual typing or percentage button.
- "isOpen" lifted to SwapForm so external surfaces can open the calc.
- ExchangeRate's no-miles slot is now an "Apply Miles" button that
  opens the calc; tooltip explains "Miles aren't available by default
  at this swap size — open the calculator to apply manually" with a
  bottom-right Learn outline button.
- BuyCard shows "Buy · miles applied" + "≈ \$<min-usd> (−<delta>)"
  when slippage was set by the calc; reverts when slippage drifts.
- TransactionSettings warning copy names the cause when miles are
  applied: "Slippage was increased to meet your miles target."

Tests:
- New small-swap-slippage.test.ts characterizing pipeline + fuzz
  invariants for auto, ratchet, and the sanity gate at small sizes.
- Updated existing slippage test expectations to the linear math.
…applied

BuyCard and SwapConfirmationModal now show the slippage-adjusted minimum
as the large white receive amount when the miles calculator has lifted
slippage above the auto baseline — keeps the primary number consistent
with the actual swap conditions instead of a stale optimistic quote. The
pre-calc estimate stays underneath as a one-line diff with a "Revert"
link that closes the calc, returns slippage to auto, and clears the
miles-applied marker.

Also:
- Close the calc when the user flips slippage mode custom→auto in
  settings (transition-tracked via ref so the default auto state
  doesn't slam the calc closed on open).
- milesToSlippage now tolerates the FLOOR_EPSILON-induced drift up to
  SLIPPAGE_MAX + 0.5%, clamping to 50% — fixes Apply being disabled
  when the user typed exactly maxAchievableMiles.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant