This commit is contained in:
2026-07-24 18:42:31 +03:00
parent d634ef4644
commit 412a065250
3 changed files with 78 additions and 12 deletions

View File

@@ -94,6 +94,28 @@ export const CHAIN_CONFIG: Record<Chain, ChainConfig> = {
}, },
} }
// ─── 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<Chain, Record<FeeTier, string>> = {
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) ────────────── // ─── Wallet ticker → chain mapping (for TokenTable integration) ──────────────
// ARB is an ERC-20 on Ethereum, not a separate chain. // ARB is an ERC-20 on Ethereum, not a separate chain.

View File

@@ -328,6 +328,31 @@
text-decoration: underline; 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 ─────────────────────────── */ /* ─── Error message ─────────────────────────── */
.errorMsg { .errorMsg {

View File

@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useSendWallet } from '@features/wallet' import { useSendWallet } from '@features/wallet'
import { getCoinIcon } from '@shared/assets/coins' 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' import styles from './SendModal.module.css'
export interface SendModalToken { export interface SendModalToken {
@@ -53,11 +54,30 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken =
const mutation = useSendWallet() const mutation = useSendWallet()
const speedLabel = SPEEDS.find((s) => s.value === speed)?.label ?? 'Нормально' 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( 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(initialToken) }, [initialToken])
useEffect(() => { setSelectedToken('') }, [network]) useEffect(() => { setSelectedToken('') }, [network])
@@ -89,14 +109,7 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken =
if (openDropdown) { setOpenDropdown(null) } else { onClose() } 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() { function handleSubmit() {
const isNative = selectedToken === ''
const convertedAmount = isNative ? toBaseUnits(amount, cfg.nativeDecimals) : amount const convertedAmount = isNative ? toBaseUnits(amount, cfg.nativeDecimals) : amount
mutation.mutate({ mutation.mutate({
chain: network, chain: network,
@@ -258,14 +271,20 @@ export function SendModal({ open, onClose, network, tokens = [], initialToken =
<button <button
type="button" type="button"
className={styles.maxBtn} className={styles.maxBtn}
onClick={() => setAmount(walletMatch.bal)} onClick={() => setAmount(maxAmount)}
> >
{walletMatch.bal} {tokenLabel} {maxAmount} {tokenLabel}
</button> </button>
</div> </div>
)} )}
</div> </div>
{/* Network fee */}
<div className={styles.feeRow}>
<span className={styles.feeLabel}>Комиссия сети</span>
<span className={styles.feeValue}> {networkFee} {cfg.nativeSymbol}</span>
</div>
{/* Error */} {/* Error */}
{mutation.isError && ( {mutation.isError && (
<div className={styles.errorMsg}> <div className={styles.errorMsg}>