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) ──────────────
|
// ─── 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user