211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
import { PrimaryButton } from '@shared/ui'
|
||
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
||
import type { Pool, LpQuote } from '@features/pools'
|
||
import { formatFeeTier } from '../model/meta'
|
||
import styles from './LpDepositCard.module.css'
|
||
|
||
interface Props {
|
||
pool: Pool | null
|
||
quote: LpQuote | undefined
|
||
quoteFetching?: boolean
|
||
pending?: boolean
|
||
amount0: string
|
||
amount1: string
|
||
priceLower: string
|
||
priceUpper: string
|
||
slippage: string
|
||
useNativeEth: boolean
|
||
onAmount0Change: (v: string) => void
|
||
onAmount1Change: (v: string) => void
|
||
onPriceLowerChange: (v: string) => void
|
||
onPriceUpperChange: (v: string) => void
|
||
onSlippageChange: (v: string) => void
|
||
onUseNativeEthChange: (v: boolean) => void
|
||
onSubmit: () => void
|
||
}
|
||
|
||
const DASH = '—'
|
||
const decimalRe = /^(\d+\.?\d*|\.?\d*)$/
|
||
|
||
function AmountInput({
|
||
symbol,
|
||
value,
|
||
onChange,
|
||
}: {
|
||
symbol: string
|
||
value: string
|
||
onChange: (v: string) => void
|
||
}) {
|
||
return (
|
||
<div className={styles.amountRow}>
|
||
<input
|
||
className={styles.amountInput}
|
||
type="text"
|
||
inputMode="decimal"
|
||
value={value}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
if (decimalRe.test(v) || v === '') onChange(v)
|
||
}}
|
||
placeholder="0"
|
||
aria-label={`Сумма ${symbol}`}
|
||
/>
|
||
<div className={styles.token}>
|
||
<span>{symbol}</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PriceInput({
|
||
label,
|
||
value,
|
||
onChange,
|
||
}: {
|
||
label: string
|
||
value: string
|
||
onChange: (v: string) => void
|
||
}) {
|
||
return (
|
||
<label className={styles.priceField}>
|
||
<span className={styles.priceLabel}>{label}</span>
|
||
<input
|
||
className={styles.priceInput}
|
||
type="text"
|
||
inputMode="decimal"
|
||
value={value}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
if (decimalRe.test(v) || v === '') onChange(v)
|
||
}}
|
||
placeholder="0"
|
||
/>
|
||
</label>
|
||
)
|
||
}
|
||
|
||
export function LpDepositCard({
|
||
pool,
|
||
quote,
|
||
quoteFetching,
|
||
pending,
|
||
amount0,
|
||
amount1,
|
||
priceLower,
|
||
priceUpper,
|
||
slippage,
|
||
useNativeEth,
|
||
onAmount0Change,
|
||
onAmount1Change,
|
||
onPriceLowerChange,
|
||
onPriceUpperChange,
|
||
onSlippageChange,
|
||
onUseNativeEthChange,
|
||
onSubmit,
|
||
}: Props) {
|
||
if (!pool) {
|
||
return (
|
||
<div className={styles.card}>
|
||
<div className={styles.placeholder}>
|
||
<p className={styles.placeholderTitle}>Выберите пул</p>
|
||
<p className={styles.placeholderText}>
|
||
Выберите пул в таблице выше, чтобы внести ликвидность.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const balance = quote?.balance
|
||
// sufficient ноги может быть null (не проверялось / покрыто нативным ETH) — это не «не хватает».
|
||
const insufficient = balance?.sufficient === false
|
||
const apr = quote?.aprPercent != null ? `${quote.aprPercent}%` : DASH
|
||
const feeUsd = quote?.appFeeTotalUsd != null ? `≈ $${quote.appFeeTotalUsd.toFixed(2)}` : DASH
|
||
const amount0Out = quote ? truncateDecimals(quote.amount0DesiredHuman, 8) : DASH
|
||
const amount1Out = quote ? truncateDecimals(quote.amount1DesiredHuman, 8) : DASH
|
||
const shortLeg = balance && balance.token1.sufficient === false ? balance.token1 : balance?.token0
|
||
const canSubmit = !!quote && !insufficient && !pending
|
||
|
||
return (
|
||
<div className={styles.card}>
|
||
<div className={styles.top}>
|
||
<span className={styles.pair}>
|
||
{pool.symbol0}/{pool.symbol1}
|
||
</span>
|
||
<span className={styles.fee}>{formatFeeTier(pool.feeTier)}</span>
|
||
</div>
|
||
|
||
<span className={styles.tag}>Суммы взноса</span>
|
||
<div className={styles.amounts}>
|
||
<AmountInput symbol={pool.symbol0} value={amount0} onChange={onAmount0Change} />
|
||
<AmountInput symbol={pool.symbol1} value={amount1} onChange={onAmount1Change} />
|
||
</div>
|
||
|
||
<span className={styles.tag}>Диапазон цен ({pool.symbol1} за {pool.symbol0})</span>
|
||
<div className={styles.range}>
|
||
<PriceInput label="Мин." value={priceLower} onChange={onPriceLowerChange} />
|
||
<PriceInput label="Макс." value={priceUpper} onChange={onPriceUpperChange} />
|
||
</div>
|
||
|
||
<div className={styles.options}>
|
||
<label className={styles.slippage}>
|
||
<span className={styles.priceLabel}>Проскальзывание, %</span>
|
||
<input
|
||
className={styles.priceInput}
|
||
type="text"
|
||
inputMode="decimal"
|
||
value={slippage}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
if (decimalRe.test(v) || v === '') onSlippageChange(v)
|
||
}}
|
||
placeholder="1"
|
||
/>
|
||
</label>
|
||
<label className={styles.toggle}>
|
||
<input
|
||
type="checkbox"
|
||
checked={useNativeEth}
|
||
onChange={(e) => onUseNativeEthChange(e.target.checked)}
|
||
/>
|
||
<span>Использовать нативный ETH</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className={styles.quote} data-loading={quoteFetching ? 'true' : undefined}>
|
||
<div className={styles.quoteRow}>
|
||
<span className={styles.quoteLabel}>Взнос {pool.symbol0}</span>
|
||
<span className={styles.quoteValue}>{amount0Out}</span>
|
||
</div>
|
||
<div className={styles.quoteRow}>
|
||
<span className={styles.quoteLabel}>Взнос {pool.symbol1}</span>
|
||
<span className={styles.quoteValue}>{amount1Out}</span>
|
||
</div>
|
||
<div className={styles.quoteRow}>
|
||
<span className={styles.quoteLabel}>Комиссия сервиса (0.7%)</span>
|
||
<span className={styles.quoteValue}>{feeUsd}</span>
|
||
</div>
|
||
<div className={styles.quoteRow}>
|
||
<span className={styles.quoteLabel}>Годовая доходность (APR)</span>
|
||
<span className={`${styles.quoteValue} ${styles.success}`}>{apr}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{insufficient && (
|
||
<p className={styles.warning}>
|
||
Недостаточно средств{shortLeg ? `: не хватает ${shortLeg.symbol}` : ''}. Пополните баланс.
|
||
</p>
|
||
)}
|
||
|
||
<div className={styles.submit}>
|
||
<PrimaryButton
|
||
label={pending ? 'Отправка…' : 'Внести в пул'}
|
||
type="button"
|
||
onClick={onSubmit}
|
||
disabled={!canSubmit}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|