fix
This commit is contained in:
@@ -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) ──────────────
|
||||
// ARB is an ERC-20 on Ethereum, not a separate chain.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
<button
|
||||
type="button"
|
||||
className={styles.maxBtn}
|
||||
onClick={() => setAmount(walletMatch.bal)}
|
||||
onClick={() => setAmount(maxAmount)}
|
||||
>
|
||||
{walletMatch.bal} {tokenLabel}
|
||||
{maxAmount} {tokenLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Network fee */}
|
||||
<div className={styles.feeRow}>
|
||||
<span className={styles.feeLabel}>Комиссия сети</span>
|
||||
<span className={styles.feeValue}>≈ {networkFee} {cfg.nativeSymbol}</span>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{mutation.isError && (
|
||||
<div className={styles.errorMsg}>
|
||||
|
||||
Reference in New Issue
Block a user