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:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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": "无法读取该二维码",
|
||||
|
||||
Reference in New Issue
Block a user