From cb9231d135bcfe982844ec8a1fb21c5a2eb666fc Mon Sep 17 00:00:00 2001 From: lemon Date: Tue, 26 May 2026 12:44:54 -0700 Subject: [PATCH] Refine wallet nav and fee helpers --- src/components/HDSendBitcoinDialog.tsx | 42 +++++++----------------- src/components/OnchainZapContent.tsx | 45 ++++---------------------- src/components/TopNav.tsx | 17 +++++++--- src/lib/bitcoinFeeSpeed.ts | 35 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 74 deletions(-) create mode 100644 src/lib/bitcoinFeeSpeed.ts diff --git a/src/components/HDSendBitcoinDialog.tsx b/src/components/HDSendBitcoinDialog.tsx index e5955b17..5cd48a0b 100644 --- a/src/components/HDSendBitcoinDialog.tsx +++ b/src/components/HDSendBitcoinDialog.tsx @@ -34,6 +34,11 @@ import { useAppContext } from '@/hooks/useAppContext'; import { useHdWalletAccess } from '@/hooks/useHdWalletAccess'; import { useHdWallet } from '@/hooks/useHdWallet'; import { notificationSuccess } from '@/lib/haptics'; +import { + getBitcoinFeeRate, + getUniqueBitcoinFeeSpeeds, + type BitcoinFeeSpeed, +} from '@/lib/bitcoinFeeSpeed'; import { isLargeAmount, nostrPubkeyToBitcoinAddress, @@ -41,7 +46,6 @@ import { } from '@/lib/bitcoin'; import { broadcastBlockbookTx, - type BlockbookFeeRates, fetchFeeRates, } from '@/lib/hdwallet/blockbook'; import { @@ -62,29 +66,7 @@ import { useQuery } from '@tanstack/react-query'; const USD_PRESETS = [1, 5, 10, 25, 100]; -type FeeSpeed = 'fastest' | 'halfHour' | 'hour' | 'economy'; - -const FEE_SPEED_ORDER: FeeSpeed[] = ['fastest', 'halfHour', 'hour', 'economy']; - -function getRateForSpeed(rates: BlockbookFeeRates, speed: FeeSpeed): number { - switch (speed) { - case 'fastest': return rates.fastestFee; - case 'halfHour': return rates.halfHourFee; - case 'hour': return rates.hourFee; - case 'economy': return rates.economyFee; - } -} - -function getUniqueFeeSpeeds(rates: BlockbookFeeRates | undefined): FeeSpeed[] { - if (!rates) return FEE_SPEED_ORDER; - const seen = new Set(); - const result: FeeSpeed[] = []; - for (const speed of FEE_SPEED_ORDER) { - const rate = getRateForSpeed(rates, speed); - if (!seen.has(rate)) { seen.add(rate); result.push(speed); } - } - return result; -} +type FeeSpeed = BitcoinFeeSpeed; // --------------------------------------------------------------------------- // Recipient resolution @@ -236,7 +218,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice }: HDSendBitcoin const currentFeeRate = useMemo(() => { if (!feeRates) return undefined; - return getRateForSpeed(feeRates, feeSpeed); + return getBitcoinFeeRate(feeRates, feeSpeed); }, [feeRates, feeSpeed]); // ── Owned UTXO set ─────────────────────────────────────────── @@ -303,12 +285,12 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice }: HDSendBitcoin if (feeSpeedUserChanged.current) return; if (!ownedInputs.length || !feeRates || amountSats <= 0) return; - const uniqueSpeeds = getUniqueFeeSpeeds(feeRates); + const uniqueSpeeds = getUniqueBitcoinFeeSpeeds(feeRates); const threshold = amountSats * 0.4; let target: FeeSpeed = uniqueSpeeds[uniqueSpeeds.length - 1]; for (const speed of uniqueSpeeds) { - const rate = getRateForSpeed(feeRates, speed); + const rate = getBitcoinFeeRate(feeRates, speed); const fee = previewHdFee(ownedInputs, amountSats, rate); if (fee > 0 && fee <= threshold) { target = speed; break; } } @@ -360,7 +342,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice }: HDSendBitcoin if (amountSats <= 0) throw new Error(t('walletSend.errors.enterAmount')); if (insufficient) throw new Error(t('walletSend.errors.insufficient')); - const rate = getRateForSpeed(feeRates, feeSpeed); + const rate = getBitcoinFeeRate(feeRates, feeSpeed); const nextChangeIndex = scan?.change.firstUnusedIndex ?? 0; setProgress('building'); @@ -578,7 +560,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice }: HDSendBitcoin
- {getUniqueFeeSpeeds(feeRates).map((speed) => ( + {getUniqueBitcoinFeeSpeeds(feeRates).map((speed) => ( diff --git a/src/components/OnchainZapContent.tsx b/src/components/OnchainZapContent.tsx index 18cd87b4..7489da2a 100644 --- a/src/components/OnchainZapContent.tsx +++ b/src/components/OnchainZapContent.tsx @@ -13,6 +13,7 @@ import { import { QRCodeCanvas } from '@/components/ui/qrcode'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { BitcoinAmountPicker } from '@/components/BitcoinAmountPicker'; +import { getBitcoinFeeRate, getUniqueBitcoinFeeSpeeds } from '@/lib/bitcoinFeeSpeed'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useBitcoinSigner } from '@/hooks/useBitcoinSigner'; @@ -41,40 +42,6 @@ const FEE_SPEED_LABELS: Record = { economy: '~1 day', }; -const FEE_SPEED_ORDER: OnchainFeeSpeed[] = ['fastest', 'halfHour', 'hour', 'economy']; - -/** - * Given the raw mempool fee rates (sat/vB), return a deduplicated list of - * speed tiers. When multiple tiers share the same rate (common when the - * mempool is empty and everything collapses to 1 sat/vB), we keep only the - * fastest-labeled tier for that rate. This prevents rows like "~10 min 2 - * sat/vB / ~30 min 2 sat/vB / ~1 hour 2 sat/vB" in the UI. - */ -function getRateForSpeed(rates: { fastestFee: number; halfHourFee: number; hourFee: number; economyFee: number }, speed: OnchainFeeSpeed): number { - switch (speed) { - case 'fastest': return rates.fastestFee; - case 'halfHour': return rates.halfHourFee; - case 'hour': return rates.hourFee; - case 'economy': return rates.economyFee; - } -} - -function getUniqueFeeSpeeds( - rates: { fastestFee: number; halfHourFee: number; hourFee: number; economyFee: number } | undefined, -): OnchainFeeSpeed[] { - if (!rates) return FEE_SPEED_ORDER; - const seen = new Set(); - const result: OnchainFeeSpeed[] = []; - for (const speed of FEE_SPEED_ORDER) { - const rate = getRateForSpeed(rates, speed); - if (!seen.has(rate)) { - seen.add(rate); - result.push(speed); - } - } - return result; -} - interface OnchainZapContentProps { target: NostrEvent; /** Called with the tx result when a zap successfully broadcasts. */ @@ -139,7 +106,7 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont const currentFeeRate = useMemo(() => { if (!feeRates) return 0; - return getRateForSpeed(feeRates, feeSpeed); + return getBitcoinFeeRate(feeRates, feeSpeed); }, [feeRates, feeSpeed]); // Convert the USD amount to sats @@ -172,12 +139,12 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont if (feeSpeedUserChanged.current) return; if (!utxos?.length || !feeRates || amountSats <= 0) return; - const uniqueSpeeds = getUniqueFeeSpeeds(feeRates); + const uniqueSpeeds = getUniqueBitcoinFeeSpeeds(feeRates); const threshold = amountSats * 0.4; let target: OnchainFeeSpeed = uniqueSpeeds[uniqueSpeeds.length - 1]; for (const speed of uniqueSpeeds) { - const rate = getRateForSpeed(feeRates, speed); + const rate = getBitcoinFeeRate(feeRates, speed); const fee2 = estimateFee(utxos.length, 2, rate); const change = totalBalance - amountSats - fee2; const outputs = change > 546 ? 2 : 1; @@ -253,7 +220,7 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont const currentUsd = typeof usdAmount === 'string' ? parseFloat(usdAmount) : usdAmount; const hasValidAmount = Number.isFinite(currentUsd) && currentUsd > 0; const totalUsdString = btcPrice ? satsToUSD(totalSats, btcPrice) : ''; - const uniqueFeeSpeeds = useMemo(() => getUniqueFeeSpeeds(feeRates), [feeRates]); + const uniqueFeeSpeeds = useMemo(() => getUniqueBitcoinFeeSpeeds(feeRates), [feeRates]); if (user && capability === 'unsupported') { return ( @@ -326,7 +293,7 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont
{uniqueFeeSpeeds.map((speed) => { - const rate = feeRates ? getRateForSpeed(feeRates, speed) : 0; + const rate = feeRates ? getBitcoinFeeRate(feeRates, speed) : 0; const selected = speed === feeSpeed; return (