feat(swap): miles calculator redesign + slippage/gas/estimator hardening#142
Open
passandscore wants to merge 11 commits intomainfrom
Open
feat(swap): miles calculator redesign + slippage/gas/estimator hardening#142passandscore wants to merge 11 commits intomainfrom
passandscore wants to merge 11 commits intomainfrom
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
maxAchievableMilesmatches what the bar will produce on Apply.isBarterValidatinggate freezes the bar + max at last-good values during token-switch transitions.slippage > autoBaselinepaint the gear amber regardless of mode.maxPriorityFeePerGas: 0n(FastSwap inclusion is paid by the bidder).computeSurplusEthsanity gate ([0.5×, 2×] of uniswap quote) anduseBarterValidationsynchronousstoredForKeygating + >90% shortfall rejection — eliminates the order-of-magnitude wrong values during token-switch transitions.Test plan
npm test— all 106 tests pass.