Add custom fee rate to wallet Send; stop showing empty fee tiers

The HD-wallet Send dialog's fee popover relied on getUniqueBitcoinFeeSpeeds
falling back to all four preset tiers when rates hadn't loaded — rendering
clickable tiers with no sat/vB value (and no way to send at all when the
Blockbook estimate API was down).

- Show loading/error status (with a Retry) in the fee popover instead of
  bare tiers when rates haven't loaded.
- Add a "Custom" fee tier with an inline sat/vB input so users can always
  specify a rate, including when the estimate API is unavailable.
- Disable Send when the resolved rate is < 1 and surface an inline error.
- Add resolveBitcoinFeeRate + a PresetBitcoinFeeSpeed type so 'custom' is
  handled distinctly from the preset tiers.
This commit is contained in:
Alex Gleason
2026-05-30 12:15:45 +02:00
parent dc43f723fb
commit d07bc64032
18 changed files with 365 additions and 50 deletions
+95 -17
View File
@@ -22,6 +22,7 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Input } from '@/components/ui/input';
import { BitcoinAmountPicker } from '@/components/BitcoinAmountPicker';
import { BitcoinPublicDisclaimer } from '@/components/BitcoinPublicDisclaimer';
import {
@@ -39,6 +40,7 @@ import { notificationSuccess } from '@/lib/haptics';
import {
getBitcoinFeeRate,
getUniqueBitcoinFeeSpeeds,
resolveBitcoinFeeRate,
type BitcoinFeeSpeed,
} from '@/lib/bitcoinFeeSpeed';
import { isLargeAmount, satsToUSD } from '@/lib/bitcoin';
@@ -136,6 +138,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
halfHour: t('walletSend.feeSpeed.halfHour'),
hour: t('walletSend.feeSpeed.hour'),
economy: t('walletSend.feeSpeed.economy'),
custom: t('walletSend.feeSpeed.custom'),
}),
[t],
);
@@ -147,6 +150,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
const [recipient, setRecipient] = useState<ResolvedRecipient | null>(null);
const [usdAmount, setUsdAmount] = useState<number | string>(5);
const [feeSpeed, setFeeSpeed] = useState<FeeSpeed>('halfHour');
/** Raw text for the custom sat/vB rate input (only used when feeSpeed === 'custom'). */
const [customFeeRate, setCustomFeeRate] = useState('');
const [error, setError] = useState('');
const [feePopoverOpen, setFeePopoverOpen] = useState(false);
const [success, setSuccess] = useState<SendResult | null>(null);
@@ -155,17 +160,22 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
// ── Fee rates ────────────────────────────────────────────────
const { data: feeRates } = useQuery({
const {
data: feeRates,
isLoading: feeRatesLoading,
isError: feeRatesError,
refetch: refetchFeeRates,
} = useQuery({
queryKey: ['blockbook-fee-rates', blockbookBaseUrl],
queryFn: ({ signal }) => fetchFeeRates(blockbookBaseUrl, signal),
enabled: isOpen && isReady,
staleTime: 30_000,
});
const currentFeeRate = useMemo(() => {
if (!feeRates) return undefined;
return getBitcoinFeeRate(feeRates, feeSpeed);
}, [feeRates, feeSpeed]);
const currentFeeRate = useMemo(
() => resolveBitcoinFeeRate(feeSpeed, feeRates, customFeeRate),
[feeSpeed, feeRates, customFeeRate],
);
// ── Owned UTXO set ───────────────────────────────────────────
//
@@ -245,7 +255,9 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
const handleFeeSpeedChange = useCallback((speed: FeeSpeed) => {
feeSpeedUserChanged.current = true;
setFeeSpeed(speed);
setFeePopoverOpen(false);
// Keep the popover open for 'custom' so the user can type a rate; close
// it for preset tiers since the choice is complete.
if (speed !== 'custom') setFeePopoverOpen(false);
}, []);
// ── Two-tap arm + raw-address disclaimer ─────────────────────
@@ -286,11 +298,12 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
}
if (!recipient) throw new Error(t('walletSend.errors.enterRecipient'));
if (!ownedInputs.length) throw new Error(t('walletSend.errors.noSpendable'));
if (!feeRates) throw new Error(t('walletSend.errors.feesNotLoaded'));
if (feeSpeed !== 'custom' && !feeRates) throw new Error(t('walletSend.errors.feesNotLoaded'));
if (amountSats <= 0) throw new Error(t('walletSend.errors.enterAmount'));
if (insufficient) throw new Error(t('walletSend.errors.insufficient'));
const rate = getBitcoinFeeRate(feeRates, feeSpeed);
const rate = resolveBitcoinFeeRate(feeSpeed, feeRates, customFeeRate);
if (!rate || rate < 1) throw new Error(t('walletSend.errors.feeRateTooLow'));
const nextChangeIndex = scan?.change.firstUnusedIndex ?? 0;
setProgress('building');
@@ -353,6 +366,14 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
if (!btcPrice) { setError(t('walletSend.errors.waitingPrice')); return; }
if (amountSats <= 0) { setError(t('walletSend.errors.enterAmount')); return; }
if (!ownedInputs.length) { setError(t('walletSend.errors.noneYet')); return; }
if (!currentFeeRate || currentFeeRate < 1) {
setError(
feeSpeed === 'custom'
? t('walletSend.errors.feeRateTooLow')
: t('walletSend.errors.feesNotLoadedYet'),
);
return;
}
if (insufficient) { setError(t('walletSend.errors.insufficient')); return; }
if (requiresArm && !confirmArmed) { setConfirmArmed(true); return; }
sendMutation.mutate();
@@ -363,6 +384,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
btcPrice,
amountSats,
ownedInputs.length,
currentFeeRate,
feeSpeed,
insufficient,
requiresArm,
confirmArmed,
@@ -378,6 +401,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
setRecipient(null);
setUsdAmount(5);
setError('');
setFeeSpeed('halfHour');
setCustomFeeRate('');
setConfirmArmed(false);
setSuccess(null);
feeSpeedUserChanged.current = false;
@@ -405,7 +430,9 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
!btcPrice ||
amountSats <= 0 ||
insufficient ||
!ownedInputs.length;
!ownedInputs.length ||
!currentFeeRate ||
currentFeeRate < 1;
// ── Render ───────────────────────────────────────────────────
return (
@@ -504,16 +531,41 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
<> {satsToUSD(estimatedFeeSats, btcPrice)}</>
) : currentFeeRate ? (
<>{t('walletSend.satPerVB', { rate: currentFeeRate })}</>
) : feeRatesLoading && feeSpeed !== 'custom' ? (
<>{t('walletSend.fee.loading')}</>
) : feeRatesError && feeSpeed !== 'custom' ? (
<>{t('walletSend.fee.unavailable')}</>
) : (
<></>
)}
<span className="opacity-60">·</span>
{feeSpeedLabels[feeSpeed]}
{feeSpeed === 'custom' && currentFeeRate
? t('walletSend.satPerVB', { rate: currentFeeRate })
: feeSpeedLabels[feeSpeed]}
</button>
</PopoverTrigger>
<PopoverContent className="w-44 p-1" align="center">
<PopoverContent className="w-56 p-1" align="center">
<div className="grid gap-0.5">
{getUniqueBitcoinFeeSpeeds(feeRates).map((speed) => (
{feeRatesError && (
<div className="px-3 py-1.5 text-xs text-muted-foreground">
<p className="text-destructive">{t('walletSend.fee.loadFailed')}</p>
<button
type="button"
onClick={() => refetchFeeRates()}
className="mt-1 underline hover:text-foreground transition-colors"
>
{t('walletSend.fee.retry')}
</button>
<p className="mt-1">{t('walletSend.fee.orCustom')}</p>
</div>
)}
{feeRatesLoading && !feeRatesError && (
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
<Loader2 className="size-3 animate-spin" />
{t('walletSend.fee.loadingTiers')}
</div>
)}
{feeRates && getUniqueBitcoinFeeSpeeds(feeRates).map((speed) => (
<button
key={speed}
type="button"
@@ -524,13 +576,39 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
)}
>
<span>{feeSpeedLabels[speed]}</span>
{feeRates && (
<span className="text-muted-foreground tabular-nums">
{t('walletSend.satPerVB', { rate: getBitcoinFeeRate(feeRates, speed) })}
</span>
)}
<span className="text-muted-foreground tabular-nums">
{t('walletSend.satPerVB', { rate: getBitcoinFeeRate(feeRates, speed) })}
</span>
</button>
))}
{/* Custom fee rate */}
<button
type="button"
onClick={() => handleFeeSpeedChange('custom')}
className={cn(
'flex justify-between items-center px-3 py-1.5 rounded-md text-xs hover:bg-muted/50 transition-colors',
feeSpeed === 'custom' && 'bg-muted',
)}
>
<span>{feeSpeedLabels.custom}</span>
</button>
{feeSpeed === 'custom' && (
<div className="flex items-center gap-1.5 px-3 py-1.5">
<Input
type="number"
inputMode="decimal"
min={1}
step={1}
autoFocus
value={customFeeRate}
onChange={(e) => setCustomFeeRate(e.target.value)}
placeholder={t('walletSend.fee.customPlaceholder')}
className="h-7 text-xs"
aria-label={t('walletSend.fee.customAriaLabel')}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">sat/vB</span>
</div>
)}
</div>
</PopoverContent>
</Popover>
+33 -4
View File
@@ -1,6 +1,14 @@
export const BITCOIN_FEE_SPEED_ORDER = ['fastest', 'halfHour', 'hour', 'economy'] as const;
export type BitcoinFeeSpeed = typeof BITCOIN_FEE_SPEED_ORDER[number];
/** The preset confirmation-speed tiers, in display order. */
export type PresetBitcoinFeeSpeed = typeof BITCOIN_FEE_SPEED_ORDER[number];
/**
* A fee selection: one of the preset tiers, or `'custom'` for a
* user-entered sat/vB rate (used when the estimate API is down or the
* user wants explicit control).
*/
export type BitcoinFeeSpeed = PresetBitcoinFeeSpeed | 'custom';
export interface BitcoinFeeRates {
fastestFee: number;
@@ -9,7 +17,7 @@ export interface BitcoinFeeRates {
economyFee: number;
}
export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: BitcoinFeeSpeed): number {
export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: PresetBitcoinFeeSpeed): number {
switch (speed) {
case 'fastest': return rates.fastestFee;
case 'halfHour': return rates.halfHourFee;
@@ -20,10 +28,10 @@ export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: BitcoinFeeSpeed
export function getUniqueBitcoinFeeSpeeds(
rates: BitcoinFeeRates | undefined,
): BitcoinFeeSpeed[] {
): PresetBitcoinFeeSpeed[] {
if (!rates) return [...BITCOIN_FEE_SPEED_ORDER];
const seen = new Set<number>();
const result: BitcoinFeeSpeed[] = [];
const result: PresetBitcoinFeeSpeed[] = [];
for (const speed of BITCOIN_FEE_SPEED_ORDER) {
const rate = getBitcoinFeeRate(rates, speed);
if (!seen.has(rate)) {
@@ -33,3 +41,24 @@ export function getUniqueBitcoinFeeSpeeds(
}
return result;
}
/**
* Resolve the effective sat/vB rate for the current selection.
*
* For `'custom'` the user-typed value wins (parsed and floored, valid only
* when ≥ 1). For preset tiers we read the loaded rates; returns `undefined`
* when rates haven't loaded (or a custom value isn't a usable rate), which
* callers should treat as "not ready" rather than a real rate.
*/
export function resolveBitcoinFeeRate(
speed: BitcoinFeeSpeed,
rates: BitcoinFeeRates | undefined,
customFeeRate: string,
): number | undefined {
if (speed === 'custom') {
const parsed = Math.floor(Number(customFeeRate));
return Number.isFinite(parsed) && parsed >= 1 ? parsed : undefined;
}
if (!rates) return undefined;
return getBitcoinFeeRate(rates, speed);
}
+15 -2
View File
@@ -1163,7 +1163,18 @@
"fastest": "~10 دقائق",
"halfHour": "~30 دقيقة",
"hour": "~ساعة",
"economy": "~يوم"
"economy": "~يوم",
"custom": "مخصّص"
},
"fee": {
"loading": "جارٍ التحميل…",
"unavailable": "غير متاح",
"loadFailed": "تعذّر تحميل معدلات الرسوم.",
"retry": "إعادة المحاولة",
"orCustom": "أو أدخل معدلاً مخصّصاً أدناه.",
"loadingTiers": "جارٍ تحميل معدلات الرسوم…",
"customPlaceholder": "مثال: 5",
"customAriaLabel": "معدل رسوم مخصّص بوحدة sat/vB"
},
"progress": {
"building": "جارٍ بناء المعاملة…",
@@ -1179,7 +1190,9 @@
"enterAmount": "أدخل مبلغاً.",
"insufficient": "البيتكوين غير كافٍ لهذا المبلغ + رسوم الشبكة.",
"waitingPrice": "في انتظار سعر BTC…",
"noneYet": "ليس لديك بيتكوين بعد."
"noneYet": "ليس لديك بيتكوين بعد.",
"feesNotLoadedYet": "لم يتم تحميل معدلات الرسوم بعد.",
"feeRateTooLow": "أدخل معدل رسوم لا يقل عن 1 sat/vB."
},
"toast": {
"failedTitle": "فشلت المعاملة"
+14 -1
View File
@@ -1677,7 +1677,18 @@
"fastest": "~10 min",
"halfHour": "~30 min",
"hour": "~1 hour",
"economy": "~1 day"
"economy": "~1 day",
"custom": "Custom"
},
"fee": {
"loading": "loading…",
"unavailable": "unavailable",
"loadFailed": "Couldn't load fee rates.",
"retry": "Retry",
"orCustom": "Or enter a custom rate below.",
"loadingTiers": "Loading fee rates…",
"customPlaceholder": "e.g. 5",
"customAriaLabel": "Custom fee rate in sat/vB"
},
"progress": {
"building": "Building transaction…",
@@ -1690,6 +1701,8 @@
"enterRecipient": "Enter a Bitcoin address or sp1… silent payment address.",
"noSpendable": "No spendable Bitcoin in this wallet.",
"feesNotLoaded": "Fee rates not loaded.",
"feesNotLoadedYet": "Fee rates haven't loaded yet.",
"feeRateTooLow": "Enter a fee rate of at least 1 sat/vB.",
"enterAmount": "Enter an amount.",
"insufficient": "Not enough Bitcoin for this amount + network fee.",
"waitingPrice": "Waiting for BTC price…",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~10 min",
"halfHour": "~30 min",
"hour": "~1 hora",
"economy": "~1 día"
"economy": "~1 día",
"custom": "Personalizada"
},
"fee": {
"loading": "cargando…",
"unavailable": "no disponible",
"loadFailed": "No se pudieron cargar las tasas de comisión.",
"retry": "Reintentar",
"orCustom": "O introduce una tasa personalizada abajo.",
"loadingTiers": "Cargando las tasas de comisión…",
"customPlaceholder": "p. ej. 5",
"customAriaLabel": "Tasa de comisión personalizada en sat/vB"
},
"progress": {
"building": "Construyendo la transacción…",
@@ -1195,7 +1206,9 @@
"enterAmount": "Introduce una cantidad.",
"insufficient": "No hay Bitcoin suficiente para esta cantidad + la comisión de red.",
"waitingPrice": "Esperando el precio del BTC…",
"noneYet": "Aún no tienes Bitcoin."
"noneYet": "Aún no tienes Bitcoin.",
"feesNotLoadedYet": "Las tasas de comisión aún no se han cargado.",
"feeRateTooLow": "Introduce una tasa de comisión de al menos 1 sat/vB."
},
"scanError": {
"title": "No se pudo leer ese código QR",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~۱۰ دقیقه",
"halfHour": "~۳۰ دقیقه",
"hour": "~۱ ساعت",
"economy": "~۱ روز"
"economy": "~۱ روز",
"custom": "سفارشی"
},
"fee": {
"loading": "در حال بارگذاری…",
"unavailable": "در دسترس نیست",
"loadFailed": "بارگذاری نرخ‌های کارمزد ممکن نشد.",
"retry": "تلاش دوباره",
"orCustom": "یا یک نرخ سفارشی را در زیر وارد کنید.",
"loadingTiers": "در حال بارگذاری نرخ‌های کارمزد…",
"customPlaceholder": "مثلاً ۵",
"customAriaLabel": "نرخ کارمزد سفارشی به sat/vB"
},
"progress": {
"building": "در حال ساخت تراکنش…",
@@ -1195,7 +1206,9 @@
"enterAmount": "یک مبلغ وارد کنید.",
"insufficient": "بیت‌کوین کافی برای این مبلغ + کارمزد شبکه ندارید.",
"waitingPrice": "در انتظار قیمت BTC…",
"noneYet": "هنوز بیت‌کوینی ندارید."
"noneYet": "هنوز بیت‌کوینی ندارید.",
"feesNotLoadedYet": "نرخ‌های کارمزد هنوز بارگذاری نشده‌اند.",
"feeRateTooLow": "یک نرخ کارمزد دست‌کم ۱ sat/vB وارد کنید."
},
"toast": {
"failedTitle": "تراکنش ناموفق بود"
+15 -2
View File
@@ -1602,7 +1602,18 @@
"fastest": "~10 min",
"halfHour": "~30 min",
"hour": "~1 heure",
"economy": "~1 jour"
"economy": "~1 jour",
"custom": "Personnalisé"
},
"fee": {
"loading": "chargement…",
"unavailable": "indisponible",
"loadFailed": "Impossible de charger les taux de frais.",
"retry": "Réessayer",
"orCustom": "Ou saisissez un taux personnalisé ci-dessous.",
"loadingTiers": "Chargement des taux de frais…",
"customPlaceholder": "p. ex. 5",
"customAriaLabel": "Taux de frais personnalisé en sat/vB"
},
"progress": {
"building": "Construction de la transaction…",
@@ -1618,7 +1629,9 @@
"enterAmount": "Saisissez un montant.",
"insufficient": "Pas assez de Bitcoin pour ce montant + frais réseau.",
"waitingPrice": "En attente du prix BTC…",
"noneYet": "Vous n'avez pas encore de Bitcoin."
"noneYet": "Vous n'avez pas encore de Bitcoin.",
"feesNotLoadedYet": "Les taux de frais ne sont pas encore chargés.",
"feeRateTooLow": "Saisissez un taux de frais d'au moins 1 sat/vB."
},
"scanError": {
"title": "Impossible de lire ce QR code",
+15 -2
View File
@@ -1548,7 +1548,18 @@
"fastest": "~10 मिनट",
"halfHour": "~30 मिनट",
"hour": "~1 घंटा",
"economy": "~1 दिन"
"economy": "~1 दिन",
"custom": "कस्टम"
},
"fee": {
"loading": "लोड हो रहा है…",
"unavailable": "अनुपलब्ध",
"loadFailed": "Fee rates लोड नहीं हो सकीं।",
"retry": "फिर कोशिश करें",
"orCustom": "या नीचे एक कस्टम रेट डालें।",
"loadingTiers": "Fee rates लोड हो रही हैं…",
"customPlaceholder": "उदा. 5",
"customAriaLabel": "sat/vB में कस्टम fee rate"
},
"progress": {
"building": "Transaction बन रहा है…",
@@ -1564,7 +1575,9 @@
"enterAmount": "एक राशि दर्ज करें।",
"insufficient": "इस राशि + नेटवर्क फ़ीस के लिए Bitcoin पर्याप्त नहीं।",
"waitingPrice": "BTC दाम का इंतज़ार है…",
"noneYet": "आपके पास अभी कोई Bitcoin नहीं है।"
"noneYet": "आपके पास अभी कोई Bitcoin नहीं है।",
"feesNotLoadedYet": "Fee rates अभी तक लोड नहीं हुई हैं।",
"feeRateTooLow": "कम से कम 1 sat/vB का fee rate डालें।"
},
"toast": {
"failedTitle": "Transaction विफल"
+15 -2
View File
@@ -1548,7 +1548,18 @@
"fastest": "~10 menit",
"halfHour": "~30 menit",
"hour": "~1 jam",
"economy": "~1 hari"
"economy": "~1 hari",
"custom": "Khusus"
},
"fee": {
"loading": "memuat…",
"unavailable": "tidak tersedia",
"loadFailed": "Tidak bisa memuat tarif biaya.",
"retry": "Coba lagi",
"orCustom": "Atau masukkan tarif khusus di bawah.",
"loadingTiers": "Memuat tarif biaya…",
"customPlaceholder": "misal 5",
"customAriaLabel": "Tarif biaya khusus dalam sat/vB"
},
"progress": {
"building": "Membangun transaksi…",
@@ -1564,7 +1575,9 @@
"enterAmount": "Masukkan jumlah.",
"insufficient": "Bitcoin tidak cukup untuk jumlah ini + biaya jaringan.",
"waitingPrice": "Menunggu harga BTC…",
"noneYet": "Anda belum punya Bitcoin."
"noneYet": "Anda belum punya Bitcoin.",
"feesNotLoadedYet": "Tarif biaya belum dimuat.",
"feeRateTooLow": "Masukkan tarif biaya minimal 1 sat/vB."
},
"scanError": {
"title": "Tidak bisa membaca kode QR itu",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~១០ នាទី",
"halfHour": "~៣០ នាទី",
"hour": "~១ ម៉ោង",
"economy": "~១ ថ្ងៃ"
"economy": "~១ ថ្ងៃ",
"custom": "ផ្ទាល់ខ្លួន"
},
"fee": {
"loading": "កំពុងផ្ទុក…",
"unavailable": "មិនអាចប្រើបាន",
"loadFailed": "មិនអាចផ្ទុកអត្រាថ្លៃបានទេ។",
"retry": "ព្យាយាមម្ដងទៀត",
"orCustom": "ឬបញ្ចូលអត្រាផ្ទាល់ខ្លួននៅខាងក្រោម។",
"loadingTiers": "កំពុងផ្ទុកអត្រាថ្លៃ…",
"customPlaceholder": "ឧ. 5",
"customAriaLabel": "អត្រាថ្លៃផ្ទាល់ខ្លួនជា sat/vB"
},
"progress": {
"building": "កំពុងបង្កើតប្រតិបត្តិការ…",
@@ -1195,7 +1206,9 @@
"enterAmount": "បញ្ចូលចំនួន។",
"insufficient": "មិនមាន Bitcoin គ្រប់គ្រាន់សម្រាប់ចំនួននេះ + ថ្លៃបណ្តាញ។",
"waitingPrice": "កំពុងរង់ចាំតម្លៃ BTC…",
"noneYet": "អ្នកមិនទាន់មាន Bitcoin ទេ។"
"noneYet": "អ្នកមិនទាន់មាន Bitcoin ទេ។",
"feesNotLoadedYet": "អត្រាថ្លៃមិនទាន់បានផ្ទុកនៅឡើយទេ។",
"feeRateTooLow": "បញ្ចូលអត្រាថ្លៃយ៉ាងហោចណាស់ 1 sat/vB។"
},
"scanError": {
"title": "មិនអាចអានកូដ QR នេះបានទេ",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~۱۰ دقیقې",
"halfHour": "~۳۰ دقیقې",
"hour": "~۱ ساعت",
"economy": "~۱ ورځ"
"economy": "~۱ ورځ",
"custom": "دودیز"
},
"fee": {
"loading": "لوډېږي…",
"unavailable": "د لاسرسي وړ نه دی",
"loadFailed": "د فیس نرخونه یې ونه لوډل شول.",
"retry": "بیا هڅه وکړئ",
"orCustom": "یا لاندې یو دودیز نرخ دننه کړئ.",
"loadingTiers": "د فیس نرخونه لوډېږي…",
"customPlaceholder": "بېلګه: 5",
"customAriaLabel": "دودیز د فیس نرخ په sat/vB کې"
},
"progress": {
"building": "د معاملې جوړول…",
@@ -1195,7 +1206,9 @@
"enterAmount": "یوه اندازه دننه کړئ.",
"insufficient": "د دې اندازې او د شبکې فیس لپاره کافي بټکوین نشته.",
"waitingPrice": "د BTC قیمت ته انتظار…",
"noneYet": "تاسو لاهم هېڅ بټکوین نه لرئ."
"noneYet": "تاسو لاهم هېڅ بټکوین نه لرئ.",
"feesNotLoadedYet": "د فیس نرخونه لاهم نه دي لوډ شوي.",
"feeRateTooLow": "لږ تر لږه د 1 sat/vB د فیس نرخ دننه کړئ."
},
"scanError": {
"title": "هغه QR کوډ ونه لوستل شو",
+14 -1
View File
@@ -1612,7 +1612,18 @@
"fastest": "~10 min",
"halfHour": "~30 min",
"hour": "~1 hora",
"economy": "~1 dia"
"economy": "~1 dia",
"custom": "Personalizada"
},
"fee": {
"loading": "carregando…",
"unavailable": "indisponível",
"loadFailed": "Não foi possível carregar as taxas.",
"retry": "Tentar novamente",
"orCustom": "Ou insira uma taxa personalizada abaixo.",
"loadingTiers": "Carregando taxas…",
"customPlaceholder": "ex. 5",
"customAriaLabel": "Taxa personalizada em sat/vB"
},
"progress": {
"building": "Construindo transação…",
@@ -1625,6 +1636,8 @@
"enterRecipient": "Digite um endereço Bitcoin ou endereço de pagamento silencioso sp1….",
"noSpendable": "Sem Bitcoin gastável nesta carteira.",
"feesNotLoaded": "Taxas não carregadas.",
"feesNotLoadedYet": "As taxas ainda não foram carregadas.",
"feeRateTooLow": "Insira uma taxa de pelo menos 1 sat/vB.",
"enterAmount": "Digite um valor.",
"insufficient": "Bitcoin insuficiente para este valor + taxa de rede.",
"waitingPrice": "Aguardando preço do BTC…",
+15 -2
View File
@@ -1612,7 +1612,18 @@
"fastest": "~10 мин",
"halfHour": "~30 мин",
"hour": "~1 час",
"economy": "~1 день"
"economy": "~1 день",
"custom": "Другая"
},
"fee": {
"loading": "загрузка…",
"unavailable": "недоступно",
"loadFailed": "Не удалось загрузить ставки комиссий.",
"retry": "Повторить",
"orCustom": "Или введите свою ставку ниже.",
"loadingTiers": "Загрузка ставок комиссий…",
"customPlaceholder": "напр. 5",
"customAriaLabel": "Своя ставка комиссии в sat/vB"
},
"progress": {
"building": "Создание транзакции…",
@@ -1628,7 +1639,9 @@
"enterAmount": "Введите сумму.",
"insufficient": "Недостаточно Bitcoin для этой суммы + комиссии сети.",
"waitingPrice": "Ожидание цены BTC…",
"noneYet": "У вас пока нет Bitcoin."
"noneYet": "У вас пока нет Bitcoin.",
"feesNotLoadedYet": "Ставки комиссий ещё не загружены.",
"feeRateTooLow": "Введите ставку комиссии не менее 1 sat/vB."
},
"scanError": {
"title": "Не удалось прочитать этот QR-код",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~10 maminitsi",
"halfHour": "~30 maminitsi",
"hour": "~awa 1",
"economy": "~zuva 1"
"economy": "~zuva 1",
"custom": "Zvakasarudzwa"
},
"fee": {
"loading": "kuloadwa…",
"unavailable": "hazviwanike",
"loadFailed": "Hatina kukwanisa kuloadha mitengo yemiripo.",
"retry": "Edza zvakare",
"orCustom": "Kana isa mutengo wakasarudzwa pazasi.",
"loadingTiers": "Kuloadha mitengo yemiripo…",
"customPlaceholder": "semuenzaniso 5",
"customAriaLabel": "Mutengo wemuripo wakasarudzwa mu-sat/vB"
},
"progress": {
"building": "Kuvaka chinoitwa…",
@@ -1195,7 +1206,9 @@
"enterAmount": "Isa huwandu.",
"insufficient": "Bitcoin haina kukwana pahuwandu uhwu + muripo wenetwork.",
"waitingPrice": "Kumirira mutengo weBTC…",
"noneYet": "Hauna kana Bitcoin parizvino."
"noneYet": "Hauna kana Bitcoin parizvino.",
"feesNotLoadedYet": "Mitengo yemiripo haisati yaloadwa.",
"feeRateTooLow": "Isa mutengo wemuripo usingadarike 1 sat/vB."
},
"toast": {
"failedTitle": "Chinoitwa chakundikana"
+15 -2
View File
@@ -1507,7 +1507,18 @@
"fastest": "~dakika 10",
"halfHour": "~dakika 30",
"hour": "~saa 1",
"economy": "~siku 1"
"economy": "~siku 1",
"custom": "Maalum"
},
"fee": {
"loading": "inapakia…",
"unavailable": "haipatikani",
"loadFailed": "Imeshindwa kupakia viwango vya ada.",
"retry": "Jaribu tena",
"orCustom": "Au weka kiwango maalum hapa chini.",
"loadingTiers": "Inapakia viwango vya ada…",
"customPlaceholder": "mf. 5",
"customAriaLabel": "Kiwango maalum cha ada katika sat/vB"
},
"progress": {
"building": "Inajenga muamala…",
@@ -1523,7 +1534,9 @@
"enterAmount": "Weka kiasi.",
"insufficient": "Hakuna Bitcoin ya kutosha kwa kiasi hiki + ada ya mtandao.",
"waitingPrice": "Inasubiri bei ya BTC…",
"noneYet": "Bado huna Bitcoin yoyote."
"noneYet": "Bado huna Bitcoin yoyote.",
"feesNotLoadedYet": "Viwango vya ada bado havijapakiwa.",
"feeRateTooLow": "Weka kiwango cha ada cha angalau 1 sat/vB."
},
"scanError": {
"title": "Imeshindwa kusoma msimbo huo wa QR",
+15 -2
View File
@@ -1547,7 +1547,18 @@
"fastest": "~10 dk",
"halfHour": "~30 dk",
"hour": "~1 saat",
"economy": "~1 gün"
"economy": "~1 gün",
"custom": "Özel"
},
"fee": {
"loading": "yükleniyor…",
"unavailable": "kullanılamıyor",
"loadFailed": "Ücret oranları yüklenemedi.",
"retry": "Yeniden dene",
"orCustom": "Ya da aşağıya özel bir oran girin.",
"loadingTiers": "Ücret oranları yükleniyor…",
"customPlaceholder": "örn. 5",
"customAriaLabel": "sat/vB cinsinden özel ücret oranı"
},
"progress": {
"building": "İşlem oluşturuluyor…",
@@ -1563,7 +1574,9 @@
"enterAmount": "Bir tutar girin.",
"insufficient": "Bu tutar + ağ ücreti için yeterli Bitcoin yok.",
"waitingPrice": "BTC fiyatı bekleniyor…",
"noneYet": "Henüz Bitcoin'iniz yok."
"noneYet": "Henüz Bitcoin'iniz yok.",
"feesNotLoadedYet": "Ücret oranları henüz yüklenmedi.",
"feeRateTooLow": "En az 1 sat/vB ücret oranı girin."
},
"toast": {
"failedTitle": "İşlem başarısız"
+14 -1
View File
@@ -1115,7 +1115,18 @@
"fastest": "~10 分鐘",
"halfHour": "~30 分鐘",
"hour": "~1 小時",
"economy": "~1 天"
"economy": "~1 天",
"custom": "自訂"
},
"fee": {
"loading": "載入中…",
"unavailable": "無法使用",
"loadFailed": "無法載入費率。",
"retry": "重試",
"orCustom": "或在下方輸入自訂費率。",
"loadingTiers": "正在載入費率…",
"customPlaceholder": "例如 5",
"customAriaLabel": "自訂費率(sat/vB"
},
"progress": {
"building": "正在構建交易…",
@@ -1128,6 +1139,8 @@
"enterRecipient": "請輸入比特幣地址或 sp1… 靜默支付地址。",
"noSpendable": "此錢包中沒有可花費的比特幣。",
"feesNotLoaded": "費率未載入。",
"feesNotLoadedYet": "費率尚未載入。",
"feeRateTooLow": "請輸入至少 1 sat/vB 的費率。",
"enterAmount": "請輸入金額。",
"insufficient": "比特幣不足以支付該金額和網路費用。",
"waitingPrice": "正在等待 BTC 價格…",
+15 -2
View File
@@ -1179,7 +1179,18 @@
"fastest": "~10 分钟",
"halfHour": "~30 分钟",
"hour": "~1 小时",
"economy": "~1 天"
"economy": "~1 天",
"custom": "自定义"
},
"fee": {
"loading": "加载中…",
"unavailable": "不可用",
"loadFailed": "无法加载费率。",
"retry": "重试",
"orCustom": "或在下方输入自定义费率。",
"loadingTiers": "正在加载费率…",
"customPlaceholder": "例如 5",
"customAriaLabel": "自定义费率(sat/vB"
},
"progress": {
"building": "正在构建交易…",
@@ -1195,7 +1206,9 @@
"enterAmount": "请输入金额。",
"insufficient": "比特币不足以支付该金额和网络费用。",
"waitingPrice": "正在等待 BTC 价格…",
"noneYet": "你还没有任何比特币。"
"noneYet": "你还没有任何比特币。",
"feesNotLoadedYet": "费率尚未加载。",
"feeRateTooLow": "请输入至少 1 sat/vB 的费率。"
},
"scanError": {
"title": "无法读取该二维码",