Pages: - add WalletLayout route (WalletHeader + main + Footer via <Outlet/>), wrap converter/swap/bridge/transactions; thin pages, drop duplicated shell CSS - extract SwapBridgeTabs shared between swap/bridge pages Converter reuse (FSD layers, no widget->widget imports): - move commission tiers to entities/commission (+ CommissionTable ui) - shared calc hook features/payment/model/useCurrencyConversion; useConverterSection becomes thin wrapper; HomePage Converter reuses it - move ConvertField/DirectionSwapButton to shared/ui; delete dead useConverter Tooling: - add eslint.config.js (ESLint 9 flat config); fix no-explicit-any in WalletPage Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import { useState } from 'react'
|
|
import { useDebounce } from '@shared/lib/hooks/useDebounce'
|
|
import { progressPercent } from '@entities/commission'
|
|
import { GAS_PRICE, MIN_RUB_AMOUNT } from '@shared/config/constants'
|
|
import type { ConvertFieldData } from '@shared/ui'
|
|
import { usePaymentConfig } from '../hooks/usePaymentConfig'
|
|
import { usePaymentQuote } from '../hooks/usePaymentQuote'
|
|
import { usePaymentQuoteByRub } from '../hooks/usePaymentQuoteByRub'
|
|
|
|
const TOO_LARGE_ERROR = 'Сумма слишком большая и превышает 600 000 ₽'
|
|
|
|
const sanitize = (raw: string) => raw.replace(/[^0-9.]/g, '')
|
|
|
|
interface Options {
|
|
/** Значение курса USDT/RUB до загрузки конфига (пилюля). */
|
|
rateFallback?: number
|
|
}
|
|
|
|
export function useCurrencyConversion({ rateFallback = 0 }: Options = {}) {
|
|
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
|
|
const [usdtInput, setUsdtInput] = useState('1000')
|
|
const [rubInput, setRubInput] = useState(String(MIN_RUB_AMOUNT))
|
|
|
|
const { data: config } = usePaymentConfig()
|
|
const configUsdtRate = Number(config?.usdt_exchange_rate) || rateFallback
|
|
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
|
|
|
|
const isUsdtToRub = direction === 'usdt_to_rub'
|
|
|
|
const numUsdt = Number.parseFloat(usdtInput) || 0
|
|
const debouncedUsdt = useDebounce(numUsdt, 400)
|
|
const { data: quoteUsdtToRub, isError: quoteError } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
|
|
|
|
const numRubInput = Number.parseFloat(rubInput) || 0
|
|
const debouncedRub = useDebounce(numRubInput, 400)
|
|
const { data: quoteRubToUsdt, isError: quoteRubError } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
|
|
|
|
const rubBelowMin = !isUsdtToRub && numRubInput > 0 && numRubInput < MIN_RUB_AMOUNT
|
|
|
|
const rubTotal = quoteUsdtToRub?.total_price ?? ''
|
|
const rubTotalNum = Number(rubTotal) || 0
|
|
const usdtFromRub = quoteRubToUsdt?.usdt_amount ?? ''
|
|
const usdtFromRubNum = Number(usdtFromRub) || 0
|
|
|
|
const commission = isUsdtToRub
|
|
? Number(quoteUsdtToRub?.service_fee) || 0
|
|
: Number(quoteRubToUsdt?.service_fee) || 0
|
|
|
|
const displayRubAmount = isUsdtToRub ? rubTotalNum : numRubInput
|
|
const effectiveRate = isUsdtToRub
|
|
? (numUsdt > 0 ? rubTotalNum / numUsdt : 0)
|
|
: (usdtFromRubNum > 0 ? numRubInput / usdtFromRubNum : 0)
|
|
|
|
function onSwap() {
|
|
setDirection(d => (d === 'usdt_to_rub' ? 'rub_to_usdt' : 'usdt_to_rub'))
|
|
}
|
|
|
|
const convert: ConvertFieldData = isUsdtToRub
|
|
? {
|
|
value: usdtInput,
|
|
currency: 'USDT',
|
|
onChange: (raw) => setUsdtInput(sanitize(raw)),
|
|
error: quoteError ? TOO_LARGE_ERROR : undefined,
|
|
}
|
|
: {
|
|
value: rubInput,
|
|
currency: 'RUB',
|
|
onChange: (raw) => setRubInput(sanitize(raw)),
|
|
error: rubBelowMin
|
|
? `Минимальная сумма: ${MIN_RUB_AMOUNT.toLocaleString('ru-RU')} ₽`
|
|
: quoteRubError
|
|
? TOO_LARGE_ERROR
|
|
: undefined,
|
|
}
|
|
|
|
const pay: ConvertFieldData = isUsdtToRub
|
|
? { value: rubTotal, currency: 'RUB' }
|
|
: { value: usdtFromRub, currency: 'USDT' }
|
|
|
|
return {
|
|
isUsdtToRub,
|
|
gasPriceRub,
|
|
configUsdtRate,
|
|
convert,
|
|
pay,
|
|
onSwap,
|
|
commission: {
|
|
amount: displayRubAmount,
|
|
progress: progressPercent(displayRubAmount),
|
|
commission,
|
|
effectiveRate,
|
|
},
|
|
// сырые значения для создания ордера и валидации в обёртках
|
|
numUsdt,
|
|
usdtFromRubNum,
|
|
rubTotal,
|
|
rubTotalNum,
|
|
numRubInput,
|
|
usdtFromRub,
|
|
rubBelowMin,
|
|
}
|
|
}
|