diff --git a/src/widgets/send-modal/model/sendTypes.ts b/src/widgets/send-modal/model/sendTypes.ts index 3ebda8e..4950c83 100644 --- a/src/widgets/send-modal/model/sendTypes.ts +++ b/src/widgets/send-modal/model/sendTypes.ts @@ -94,6 +94,28 @@ export const CHAIN_CONFIG: Record = { }, } +// ─── Network-fee estimates ─────────────────────────────────────────────────── +// Rough, static estimates of the network fee for a single transfer, expressed in +// the chain's native coin (human units). Used to (a) reserve the fee in the "Макс" +// button for native sends and (b) show an approximate cost in the send modal. +// Real fees are computed by the backend at broadcast time — these are intentionally +// conservative placeholders and can be swapped for a live estimate endpoint later. + +export const NETWORK_FEE_ESTIMATE: Record> = { + ETH: { slow: '0.00021', normal: '0.00042', fast: '0.00084' }, + BSC: { slow: '0.00021', normal: '0.00021', fast: '0.00021' }, + BTC: { slow: '0.00002', normal: '0.00007', fast: '0.0002' }, + TRX: { slow: '1.1', normal: '1.1', fast: '1.1' }, + SOL: { slow: '0.000005', normal: '0.000005', fast: '0.000005' }, +} + +/** Estimated native-coin network fee for a chain at the given speed tier. + * Chains without fee tiers ignore `tier` and always use the `normal` value. */ +export function estimateNetworkFee(chain: Chain, tier: FeeTier): string { + const perTier = NETWORK_FEE_ESTIMATE[chain] + return CHAIN_CONFIG[chain].hasFeeTier ? perTier[tier] : perTier.normal +} + // ─── Wallet ticker → chain mapping (for TokenTable integration) ────────────── // ARB is an ERC-20 on Ethereum, not a separate chain. diff --git a/src/widgets/send-modal/ui/SendModal.module.css b/src/widgets/send-modal/ui/SendModal.module.css index 27664b0..4ce3392 100644 --- a/src/widgets/send-modal/ui/SendModal.module.css +++ b/src/widgets/send-modal/ui/SendModal.module.css @@ -328,6 +328,31 @@ text-decoration: underline; } +/* ─── Network fee ───────────────────────────── */ + +.feeRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 12px 14px; + margin-bottom: 12px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 12px; + font-size: 13px; +} + +.feeLabel { + color: var(--text-secondary); +} + +.feeValue { + font-family: var(--font-mono); + font-weight: 600; + color: var(--text-primary); +} + /* ─── Error message ─────────────────────────── */ .errorMsg { diff --git a/src/widgets/send-modal/ui/SendModal.tsx b/src/widgets/send-modal/ui/SendModal.tsx index 4428191..7c3149b 100644 --- a/src/widgets/send-modal/ui/SendModal.tsx +++ b/src/widgets/send-modal/ui/SendModal.tsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import { useSendWallet } from '@features/wallet' import { getCoinIcon } from '@shared/assets/coins' -import { CHAIN_CONFIG, CHAIN_TOKENS, type Chain, type FeeTier } from '../model/sendTypes' +import { toBaseUnits, fromBaseUnits } from '@shared/lib/utils/baseUnits' +import { CHAIN_CONFIG, CHAIN_TOKENS, estimateNetworkFee, type Chain, type FeeTier } from '../model/sendTypes' import styles from './SendModal.module.css' export interface SendModalToken { @@ -53,11 +54,30 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken = const mutation = useSendWallet() const speedLabel = SPEEDS.find((s) => s.value === speed)?.label ?? 'Нормально' - const tokenLabel = selectedToken === '' ? cfg.nativeSymbol : selectedToken + const isNative = selectedToken === '' + const tokenLabel = isNative ? cfg.nativeSymbol : selectedToken const walletMatch = tokens.find( - (t) => t.ticker === (selectedToken === '' ? cfg.nativeSymbol : selectedToken), + (t) => t.ticker === (isNative ? cfg.nativeSymbol : selectedToken), ) + // Estimated native-coin network fee for the current chain/speed. Shown at the + // bottom of the modal and reserved by "Макс" for native sends. + const networkFee = estimateNetworkFee(network, speed) + + // Max amount the user can send. For native coins the fee is paid from the same + // balance, so reserve it (balance − fee, floored at 0). Token sends pay the fee + // in the native coin, so the full token balance stays sendable. + function maxSendable(): string { + if (!walletMatch) return '' + if (!isNative) return walletMatch.bal + const dec = cfg.nativeDecimals + const balBase = BigInt(toBaseUnits(walletMatch.bal, dec)) + const feeBase = BigInt(toBaseUnits(networkFee, dec)) + const net = balBase > feeBase ? balBase - feeBase : 0n + return fromBaseUnits(net.toString(), dec) + } + const maxAmount = maxSendable() + useEffect(() => { setSelectedToken(initialToken) }, [initialToken]) useEffect(() => { setSelectedToken('') }, [network]) @@ -89,14 +109,7 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken = if (openDropdown) { setOpenDropdown(null) } else { onClose() } } - function toBaseUnits(value: string, decimals: number): string { - const [intPart, fracPart = ''] = value.split('.') - const frac = fracPart.slice(0, decimals).padEnd(decimals, '0') - return (BigInt(intPart || '0') * (10n ** BigInt(decimals)) + BigInt(frac || '0')).toString() - } - function handleSubmit() { - const isNative = selectedToken === '' const convertedAmount = isNative ? toBaseUnits(amount, cfg.nativeDecimals) : amount mutation.mutate({ chain: network, @@ -258,14 +271,20 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken = )} + {/* Network fee */} +
+ Комиссия сети + ≈ {networkFee} {cfg.nativeSymbol} +
+ {/* Error */} {mutation.isError && (