init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
338
webapp/components/lending/repay/RepayBorrowDebt.tsx
Normal file
338
webapp/components/lending/repay/RepayBorrowDebt.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@heroui/react";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { buttonStyles } from "@/lib/buttonStyles";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useReadContract } from "wagmi";
|
||||
import { formatUnits } from "viem";
|
||||
import { useLendingSupply, useSuppliedBalance } from "@/hooks/useLendingSupply";
|
||||
import { useLendingWithdraw } from "@/hooks/useLendingWithdraw";
|
||||
import { useBorrowBalance, useMaxBorrowable } from "@/hooks/useCollateral";
|
||||
import { notifyLendingBorrow, notifyLendingRepay } from "@/lib/api/lending";
|
||||
import { useAppKit } from "@reown/appkit/react";
|
||||
import { toast } from "sonner";
|
||||
import { getTxUrl, getContractAddress, abis } from "@/lib/contracts";
|
||||
import { useTokenDecimalsFromAddress } from "@/hooks/useTokenDecimals";
|
||||
import { useTokenBySymbol } from "@/hooks/useTokenBySymbol";
|
||||
|
||||
interface RepayBorrowDebtProps {
|
||||
refreshTrigger?: number;
|
||||
}
|
||||
|
||||
export default function RepayBorrowDebt({ refreshTrigger }: RepayBorrowDebtProps) {
|
||||
const { t } = useApp();
|
||||
const { isConnected } = useAccount();
|
||||
const { open } = useAppKit();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'borrow' | 'repay'>('borrow');
|
||||
const [amount, setAmount] = useState('');
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
// On-chain data
|
||||
const { formattedBalance: suppliedBalance, refetch: refetchSupply } = useSuppliedBalance();
|
||||
const { formattedBalance: borrowedBalance, refetch: refetchBorrow } = useBorrowBalance();
|
||||
const { formattedAvailable, available, refetch: refetchMaxBorrowable } = useMaxBorrowable();
|
||||
|
||||
// Refresh when collateral changes (triggered by RepaySupplyCollateral)
|
||||
useEffect(() => {
|
||||
if (refreshTrigger === undefined || refreshTrigger === 0) return;
|
||||
refetchBorrow();
|
||||
refetchMaxBorrowable();
|
||||
}, [refreshTrigger]);
|
||||
|
||||
// Borrow = withdraw (takes USDC from protocol, increasing debt)
|
||||
const {
|
||||
status: borrowStatus,
|
||||
error: borrowError,
|
||||
isLoading: isBorrowing,
|
||||
withdrawHash: borrowHash,
|
||||
executeWithdraw: executeBorrow,
|
||||
reset: resetBorrow,
|
||||
} = useLendingWithdraw();
|
||||
|
||||
// Repay = supply USDC (reduces debt)
|
||||
const {
|
||||
status: repayStatus,
|
||||
error: repayError,
|
||||
isLoading: isRepaying,
|
||||
approveHash: repayApproveHash,
|
||||
supplyHash: repayHash,
|
||||
executeApproveAndSupply: executeRepay,
|
||||
reset: resetRepay,
|
||||
} = useLendingSupply();
|
||||
|
||||
const BORROW_TOAST_ID = 'lending-borrow-tx';
|
||||
const REPAY_TOAST_ID = 'lending-repay-tx';
|
||||
|
||||
// Borrow 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (borrowHash && borrowStatus === 'withdrawing') {
|
||||
toast.loading(t("repay.toast.borrowSubmitted"), {
|
||||
id: BORROW_TOAST_ID,
|
||||
description: t("repay.toast.borrowingNow"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(borrowHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [borrowHash, borrowStatus]);
|
||||
|
||||
// Repay approve 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (repayApproveHash && repayStatus === 'approving') {
|
||||
toast.loading(t("repay.toast.approvalSubmitted"), {
|
||||
id: REPAY_TOAST_ID,
|
||||
description: t("repay.toast.waitingConfirmation"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(repayApproveHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [repayApproveHash, repayStatus]);
|
||||
|
||||
// Repay 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (repayHash && repayStatus === 'supplying') {
|
||||
toast.loading(t("repay.toast.repaySubmitted"), {
|
||||
id: REPAY_TOAST_ID,
|
||||
description: t("repay.toast.repayingNow"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(repayHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [repayHash, repayStatus]);
|
||||
|
||||
// Notify backend after borrow
|
||||
useEffect(() => {
|
||||
if (borrowStatus === 'success' && borrowHash && amount) {
|
||||
toast.success(t("repay.toast.borrowSuccess"), {
|
||||
id: BORROW_TOAST_ID,
|
||||
description: t("repay.toast.borrowSuccessDesc"),
|
||||
duration: 5000,
|
||||
});
|
||||
notifyLendingBorrow(amount, borrowHash);
|
||||
setTimeout(() => refetchBorrow(), 2000);
|
||||
setAmount('');
|
||||
setTimeout(resetBorrow, 3000);
|
||||
}
|
||||
}, [borrowStatus]);
|
||||
|
||||
// Notify backend after repay
|
||||
useEffect(() => {
|
||||
if (repayStatus === 'success' && repayHash && amount) {
|
||||
toast.success(t("repay.toast.repaySuccess"), {
|
||||
id: REPAY_TOAST_ID,
|
||||
description: t("repay.toast.repaySuccessDesc"),
|
||||
duration: 5000,
|
||||
});
|
||||
notifyLendingRepay(amount, repayHash);
|
||||
setTimeout(() => { refetchBorrow(); refetchSupply(); }, 2000);
|
||||
setAmount('');
|
||||
setTimeout(resetRepay, 3000);
|
||||
}
|
||||
}, [repayStatus]);
|
||||
|
||||
// 错误提示
|
||||
useEffect(() => {
|
||||
if (borrowError) {
|
||||
if (borrowError === 'Transaction cancelled') {
|
||||
toast.dismiss(BORROW_TOAST_ID);
|
||||
} else {
|
||||
toast.error(t("repay.toast.borrowFailed"), { id: BORROW_TOAST_ID, duration: 5000 });
|
||||
}
|
||||
}
|
||||
}, [borrowError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (repayError) {
|
||||
if (repayError === 'Transaction cancelled') {
|
||||
toast.dismiss(REPAY_TOAST_ID);
|
||||
} else {
|
||||
toast.error(t("repay.toast.repayFailed"), { id: REPAY_TOAST_ID, duration: 5000 });
|
||||
}
|
||||
}
|
||||
}, [repayError]);
|
||||
|
||||
// 从产品 API 获取 USDC token 信息,优先从合约地址读取精度
|
||||
const usdcToken = useTokenBySymbol('USDC');
|
||||
const { chainId } = useAccount();
|
||||
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined;
|
||||
const usdcInputDecimals = useTokenDecimalsFromAddress(usdcToken?.contractAddress, usdcToken?.decimals ?? 18);
|
||||
|
||||
// 最小借贷数量
|
||||
const { data: baseBorrowMinRaw } = useReadContract({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'baseBorrowMin',
|
||||
query: { enabled: !!lendingProxyAddress },
|
||||
});
|
||||
const baseBorrowMin = baseBorrowMinRaw != null
|
||||
? parseFloat(formatUnits(baseBorrowMinRaw as bigint, usdcInputDecimals))
|
||||
: 0;
|
||||
const usdcDisplayDecimals = Math.min(usdcInputDecimals, 6);
|
||||
const truncateDecimals = (s: string, d: number) => { const p = s.split('.'); return p.length > 1 ? `${p[0]}.${p[1].slice(0, d)}` : s; };
|
||||
const isValidAmount = (v: string) => v !== '' && !isNaN(parseFloat(v)) && parseFloat(v) > 0;
|
||||
const isBelowMin = activeTab === 'borrow' && isValidAmount(amount) && baseBorrowMin > 0 && parseFloat(amount) < baseBorrowMin;
|
||||
|
||||
const handleAmountChange = (value: string) => {
|
||||
if (value === '') { setAmount(value); return; }
|
||||
if (!/^\d*\.?\d*$/.test(value)) return;
|
||||
const parts = value.split('.');
|
||||
if (parts.length > 1 && parts[1].length > usdcDisplayDecimals) return;
|
||||
const maxBalance = activeTab === 'borrow' ? parseFloat(formattedAvailable) : parseFloat(borrowedBalance);
|
||||
if (parseFloat(value) > maxBalance) {
|
||||
setAmount(truncateDecimals(activeTab === 'borrow' ? formattedAvailable : borrowedBalance, usdcDisplayDecimals));
|
||||
return;
|
||||
}
|
||||
setAmount(value);
|
||||
};
|
||||
|
||||
const handleAction = () => {
|
||||
if (!amount || parseFloat(amount) <= 0) return;
|
||||
if (activeTab === 'borrow') {
|
||||
executeBorrow(amount);
|
||||
} else {
|
||||
executeRepay(amount);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMax = () => {
|
||||
if (activeTab === 'repay') {
|
||||
setAmount(truncateDecimals(borrowedBalance, usdcDisplayDecimals));
|
||||
} else if (activeTab === 'borrow') {
|
||||
setAmount(truncateDecimals(formattedAvailable, usdcDisplayDecimals));
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isBorrowing || isRepaying;
|
||||
const currentStatus = activeTab === 'borrow' ? borrowStatus : repayStatus;
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (!isConnected) return t("common.connectWallet");
|
||||
if (currentStatus === 'approving') return t("repay.approvingUsdc");
|
||||
if (currentStatus === 'approved') return t("repay.confirmedProcessing");
|
||||
if (currentStatus === 'withdrawing') return t("repay.borrowing");
|
||||
if (currentStatus === 'supplying') return t("repay.repaying");
|
||||
if (currentStatus === 'success') return t("common.success");
|
||||
if (currentStatus === 'error') return t("common.failed");
|
||||
if (amount && !isValidAmount(amount)) return t("common.invalidAmount");
|
||||
if (isBelowMin) return t("repay.minBorrow").replace('{{amount}}', baseBorrowMin.toLocaleString('en-US', { maximumFractionDigits: usdcDisplayDecimals }));
|
||||
return activeTab === 'borrow' ? t("repay.borrow") : t("repay.repay");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-6 flex-1 shadow-md">
|
||||
{/* Title */}
|
||||
<h3 className="text-body-large font-bold text-text-primary dark:text-white leading-[150%]">
|
||||
{t("repay.borrowDebt")}
|
||||
</h3>
|
||||
|
||||
{/* USDC Balance Info */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full overflow-hidden shadow-md flex-shrink-0">
|
||||
<Image
|
||||
src="/assets/tokens/usd-coin-usdc-logo-10.svg"
|
||||
alt="USDC"
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-heading-h4 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em] truncate">
|
||||
{!mounted ? '--' : parseFloat(truncateDecimals(borrowedBalance, usdcDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: usdcDisplayDecimals })}
|
||||
</span>
|
||||
<span className="text-body-small font-bold text-text-tertiary dark:text-gray-400 flex-shrink-0">USDC</span>
|
||||
</div>
|
||||
<span className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
||||
{t("repay.borrowed")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end flex-shrink-0">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-body-small font-bold text-[#10b981] dark:text-green-400 whitespace-nowrap">
|
||||
{!mounted ? '--' : parseFloat(truncateDecimals(formattedAvailable, usdcDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: usdcDisplayDecimals })}
|
||||
</span>
|
||||
<span className="text-caption-tiny font-bold text-[#10b981] dark:text-green-400 flex-shrink-0">USDC</span>
|
||||
</div>
|
||||
<span className="text-caption-tiny font-medium text-text-tertiary dark:text-gray-400 whitespace-nowrap">
|
||||
{t("repay.availableToBorrow")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex rounded-xl bg-fill-secondary-click dark:bg-gray-700 p-1">
|
||||
<button
|
||||
className={`flex-1 rounded-lg py-2 text-body-small font-bold transition-all ${
|
||||
activeTab === 'borrow'
|
||||
? 'bg-white dark:bg-gray-600 text-text-primary dark:text-white shadow-sm'
|
||||
: 'text-text-tertiary dark:text-gray-400'
|
||||
}`}
|
||||
onClick={() => { setActiveTab('borrow'); setAmount(''); resetBorrow(); }}
|
||||
>
|
||||
{t("repay.borrow")}
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 rounded-lg py-2 text-body-small font-bold transition-all ${
|
||||
activeTab === 'repay'
|
||||
? 'bg-white dark:bg-gray-600 text-text-primary dark:text-white shadow-sm'
|
||||
: 'text-text-tertiary dark:text-gray-400'
|
||||
}`}
|
||||
onClick={() => { setActiveTab('repay'); setAmount(''); resetRepay(); }}
|
||||
>
|
||||
{t("repay.repay")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-caption-tiny font-medium text-text-tertiary dark:text-gray-400">
|
||||
{t("repay.amount")} (USDC)
|
||||
</span>
|
||||
{activeTab === 'borrow' && mounted && available > 0 && (
|
||||
<button
|
||||
onClick={handleMax}
|
||||
className="text-caption-tiny font-bold text-[#ff6900] hover:opacity-80"
|
||||
>
|
||||
{t("common.max")}: {parseFloat(truncateDecimals(formattedAvailable, usdcDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: usdcDisplayDecimals })} USDC
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'repay' && mounted && parseFloat(borrowedBalance) > 0 && (
|
||||
<button
|
||||
onClick={handleMax}
|
||||
className="text-caption-tiny font-bold text-[#ff6900] hover:opacity-80"
|
||||
>
|
||||
{t("common.max")}: {parseFloat(truncateDecimals(borrowedBalance, usdcDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: usdcDisplayDecimals })} USDC
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center bg-bg-subtle dark:bg-gray-700 rounded-xl px-4 h-[52px] gap-2">
|
||||
<input
|
||||
type="text" inputMode="decimal"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => handleAmountChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-body-default font-bold text-text-primary dark:text-white outline-none"
|
||||
/>
|
||||
<span className="text-body-small font-bold text-text-tertiary dark:text-gray-400">
|
||||
USDC
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<Button
|
||||
isDisabled={!mounted || (isConnected && (!isValidAmount(amount) || isBelowMin || isLoading || currentStatus === 'success'))}
|
||||
color="default"
|
||||
className={buttonStyles({ intent: "theme" })}
|
||||
onPress={() => !isConnected ? open() : handleAction()}
|
||||
>
|
||||
{!mounted ? 'Loading...' : getButtonLabel()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
102
webapp/components/lending/repay/RepayHeader.tsx
Normal file
102
webapp/components/lending/repay/RepayHeader.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { useYTPrice } from "@/hooks/useCollateral";
|
||||
import { fetchLendingStats } from "@/lib/api/lending";
|
||||
import { useTokenBySymbol } from "@/hooks/useTokenBySymbol";
|
||||
|
||||
const GRADIENT_COLORS: Record<string, string> = {
|
||||
'YT-A': 'linear-gradient(135deg, #FF8904 0%, #F54900 100%)',
|
||||
'YT-B': 'linear-gradient(135deg, #00BBA7 0%, #007A55 100%)',
|
||||
'YT-C': 'linear-gradient(135deg, #667EEA 0%, #764BA2 100%)',
|
||||
};
|
||||
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #374151 100%)';
|
||||
|
||||
interface RepayHeaderProps {
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export default function RepayHeader({ tokenType }: RepayHeaderProps) {
|
||||
const { t } = useApp();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [availableUsd, setAvailableUsd] = useState<string | null>(null);
|
||||
|
||||
const ytToken = useTokenBySymbol(tokenType);
|
||||
const { formattedPrice, isLoading: isPriceLoading } = useYTPrice(ytToken);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
fetchLendingStats().then((stats) => {
|
||||
if (stats) {
|
||||
const available = stats.totalSuppliedUsd - stats.totalBorrowedUsd;
|
||||
setAvailableUsd(available > 0 ? available.toLocaleString('en-US', { maximumFractionDigits: 0 }) : '0');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const displayPrice = !mounted || isPriceLoading ? '--' : `$${parseFloat(formattedPrice).toFixed(4)}`;
|
||||
const displayAvailable = availableUsd !== null ? `$${availableUsd}` : '--';
|
||||
|
||||
return (
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 px-4 md:px-6 py-4 md:py-8 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
{/* Left Section - Token Icons and Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Overlapping Token Icons */}
|
||||
<div className="flex items-center relative">
|
||||
<div
|
||||
className="w-[52px] h-[52px] rounded-full flex items-center justify-center text-white text-xs font-bold shadow-md relative z-10 overflow-hidden"
|
||||
style={{ background: GRADIENT_COLORS[tokenType] ?? DEFAULT_GRADIENT }}
|
||||
>
|
||||
{ytToken?.iconUrl ? (
|
||||
<Image src={ytToken.iconUrl} alt={tokenType} width={52} height={52} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
tokenType.replace('YT-', '')
|
||||
)}
|
||||
</div>
|
||||
<Image
|
||||
src="/assets/tokens/usd-coin-usdc-logo-10.svg"
|
||||
alt="USDC"
|
||||
width={52}
|
||||
height={52}
|
||||
className="relative -ml-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title and Subtitle */}
|
||||
<div className="flex flex-col gap-1 ml-2">
|
||||
<h1 className="text-heading-h2 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.01em]">
|
||||
{tokenType} / USDC
|
||||
</h1>
|
||||
<p className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
||||
{t("repay.supplyToBorrow")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Stats */}
|
||||
<div className="flex items-center justify-between md:w-[262px]">
|
||||
{/* Price */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
||||
{t("repay.price")}
|
||||
</span>
|
||||
<span className="text-heading-h3 font-bold text-[#10b981] dark:text-green-400 leading-[130%] tracking-[-0.005em]">
|
||||
{displayPrice}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Available */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
||||
{t("repay.available")}
|
||||
</span>
|
||||
<span className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
|
||||
{displayAvailable}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
webapp/components/lending/repay/RepayPoolStats.tsx
Normal file
88
webapp/components/lending/repay/RepayPoolStats.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { fetchProducts, fetchProductDetail } from "@/lib/api/fundmarket";
|
||||
|
||||
interface RepayPoolStatsProps {
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export default function RepayPoolStats({ tokenType }: RepayPoolStatsProps) {
|
||||
const { t } = useApp();
|
||||
|
||||
const { data: products = [] } = useQuery({
|
||||
queryKey: ['token-list'],
|
||||
queryFn: () => fetchProducts(true),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const product = products.find(p => p.tokenSymbol === tokenType);
|
||||
|
||||
const { data: detail } = useQuery({
|
||||
queryKey: ['product-detail', product?.id],
|
||||
queryFn: () => fetchProductDetail(product!.id),
|
||||
enabled: !!product?.id,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const tvlDisplay = detail?.tvlUsd != null
|
||||
? `$${detail.tvlUsd.toLocaleString('en-US', { maximumFractionDigits: 0 })}`
|
||||
: '--';
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
|
||||
{/* Left Card - Total Value Locked and Utilization */}
|
||||
<div className="bg-white/50 dark:bg-gray-800/50 rounded-2xl border border-border-normal dark:border-gray-700 px-4 md:px-6 py-4 flex items-center justify-between md:h-[98px]">
|
||||
{/* Total Value Locked */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[12px] font-bold text-[#90a1b9] dark:text-gray-400 leading-4 uppercase">
|
||||
{t("repay.totalValueLocked")}
|
||||
</span>
|
||||
<span className="text-[20px] font-bold text-[#0f172b] dark:text-white leading-7 tracking-[-0.45px]">
|
||||
{tvlDisplay}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Utilization */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[12px] font-bold text-[#90a1b9] dark:text-gray-400 leading-4 uppercase text-right">
|
||||
{t("repay.utilization")}
|
||||
</span>
|
||||
<span className="text-[20px] font-bold text-[#0f172b] dark:text-white leading-7 tracking-[-0.45px] text-right">
|
||||
42.8%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Card - Reward Multiplier */}
|
||||
<div className="bg-white/50 dark:bg-gray-800/50 rounded-2xl border border-border-normal dark:border-gray-700 px-4 md:px-6 py-4 flex items-center justify-between md:h-[98px]">
|
||||
{/* Reward Multiplier */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[12px] font-bold text-[#90a1b9] dark:text-gray-400 leading-4 uppercase">
|
||||
{t("repay.rewardMultiplier")}
|
||||
</span>
|
||||
<span className="text-[20px] font-bold text-[#0f172b] dark:text-white leading-7 tracking-[-0.45px]">
|
||||
2.5x
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Overlapping Circles */}
|
||||
<div className="relative w-20 h-8">
|
||||
{/* Green Circle */}
|
||||
<div className="absolute left-0 top-0 w-8 h-8 rounded-full bg-[#00bc7d] border-2 border-white dark:border-gray-900" />
|
||||
|
||||
{/* Blue Circle */}
|
||||
<div className="absolute left-6 top-0 w-8 h-8 rounded-full bg-[#2b7fff] border-2 border-white dark:border-gray-900" />
|
||||
|
||||
{/* Gray Circle with +3 */}
|
||||
<div className="absolute left-12 top-0 w-8 h-8 rounded-full bg-[#e2e8f0] dark:bg-gray-600 border-2 border-white dark:border-gray-900 flex items-center justify-center">
|
||||
<span className="text-[10px] font-bold text-[#45556c] dark:text-gray-300 leading-[15px] tracking-[0.12px]">
|
||||
+3
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
webapp/components/lending/repay/RepayStats.tsx
Normal file
93
webapp/components/lending/repay/RepayStats.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { useLTV, useBorrowBalance } from "@/hooks/useCollateral";
|
||||
import { useSupplyAPY } from "@/hooks/useHealthFactor";
|
||||
|
||||
export default function RepayStats() {
|
||||
const { t } = useApp();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { ltvRaw } = useLTV();
|
||||
const { formattedBalance: borrowedBalance } = useBorrowBalance();
|
||||
const { apr: supplyApr } = useSupplyAPY();
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
const LIQUIDATION_LTV = 75;
|
||||
const hasPosition = mounted && parseFloat(borrowedBalance) > 0;
|
||||
const healthPct = hasPosition ? Math.max(0, Math.min(100, (1 - ltvRaw / LIQUIDATION_LTV) * 100)) : 0;
|
||||
|
||||
const getHealthLabel = () => {
|
||||
if (!hasPosition) return 'No Position';
|
||||
if (ltvRaw < 50) return `Safe ${healthPct.toFixed(0)}%`;
|
||||
if (ltvRaw < 65) return `Warning ${healthPct.toFixed(0)}%`;
|
||||
return `At Risk ${healthPct.toFixed(0)}%`;
|
||||
};
|
||||
|
||||
const getHealthColor = () => {
|
||||
if (!hasPosition) return 'text-text-tertiary dark:text-gray-400';
|
||||
if (ltvRaw < 50) return 'text-[#10b981] dark:text-green-400';
|
||||
if (ltvRaw < 65) return 'text-[#ff6900] dark:text-orange-400';
|
||||
return 'text-[#ef4444] dark:text-red-400';
|
||||
};
|
||||
|
||||
const getBarGradient = () => {
|
||||
if (!hasPosition) return undefined;
|
||||
if (ltvRaw < 50) return 'linear-gradient(90deg, rgba(0, 213, 190, 1) 0%, rgba(0, 188, 125, 1) 100%)';
|
||||
if (ltvRaw < 65) return 'linear-gradient(90deg, #ff6900 0%, #ff9900 100%)';
|
||||
return 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)';
|
||||
};
|
||||
|
||||
// Net APR = getSupplyRate() directly (already represents net yield)
|
||||
const netApr = supplyApr > 0 ? supplyApr.toFixed(4) : null;
|
||||
|
||||
return (
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-8 flex-1 shadow-md">
|
||||
{/* Stats Info */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* NET APR */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Image src="/components/repay/icon0.svg" alt="" width={18} height={18} className="flex-shrink-0" />
|
||||
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
||||
{t("repay.netApr")}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`text-body-small font-bold leading-[150%] whitespace-nowrap flex-shrink-0 ${netApr !== null && parseFloat(netApr) >= 0 ? 'text-[#10b981] dark:text-green-400' : 'text-[#ef4444]'}`}>
|
||||
{netApr !== null ? `${parseFloat(netApr) >= 0 ? '+' : ''}${netApr}%` : '--'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Position Health */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Image src="/components/repay/icon2.svg" alt="" width={18} height={18} className="flex-shrink-0" />
|
||||
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
||||
{t("repay.positionHealth")}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`text-body-small font-bold leading-[150%] whitespace-nowrap flex-shrink-0 ${getHealthColor()}`}>
|
||||
{!mounted ? '--' : getHealthLabel()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full">
|
||||
<div className="w-full h-2 bg-[#f3f4f6] dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${hasPosition ? healthPct : 0}%`,
|
||||
background: getBarGradient() || '#e5e7eb',
|
||||
boxShadow: hasPosition && ltvRaw < 50 ? "0px 0px 15px 0px rgba(16, 185, 129, 0.3)" : undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
webapp/components/lending/repay/RepaySupplyCollateral.tsx
Normal file
309
webapp/components/lending/repay/RepaySupplyCollateral.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@heroui/react";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { buttonStyles } from "@/lib/buttonStyles";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useLendingCollateral, useCollateralBalance, useWithdrawCollateral, useYTWalletBalance } from "@/hooks/useLendingCollateral";
|
||||
import { useYTPrice } from "@/hooks/useCollateral";
|
||||
import { notifySupplyCollateral, notifyWithdrawCollateral } from "@/lib/api/lending";
|
||||
import { useAppKit } from "@reown/appkit/react";
|
||||
import { toast } from "sonner";
|
||||
import { getTxUrl } from "@/lib/contracts";
|
||||
import { useTokenBySymbol } from "@/hooks/useTokenBySymbol";
|
||||
|
||||
const GRADIENT_COLORS: Record<string, string> = {
|
||||
'YT-A': 'linear-gradient(135deg, #FF8904 0%, #F54900 100%)',
|
||||
'YT-B': 'linear-gradient(135deg, #00BBA7 0%, #007A55 100%)',
|
||||
'YT-C': 'linear-gradient(135deg, #667EEA 0%, #764BA2 100%)',
|
||||
};
|
||||
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #374151 100%)';
|
||||
|
||||
interface RepaySupplyCollateralProps {
|
||||
tokenType: string;
|
||||
onCollateralChanged?: () => void;
|
||||
}
|
||||
|
||||
export default function RepaySupplyCollateral({ tokenType, onCollateralChanged }: RepaySupplyCollateralProps) {
|
||||
const { t } = useApp();
|
||||
const { isConnected } = useAccount();
|
||||
const { open } = useAppKit();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'deposit' | 'withdraw'>('deposit');
|
||||
const [amount, setAmount] = useState('');
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
// On-chain data
|
||||
const ytToken = useTokenBySymbol(tokenType);
|
||||
const { formattedBalance, refetch: refetchBalance } = useCollateralBalance(ytToken);
|
||||
const { formattedBalance: walletBalance, refetch: refetchWalletBalance } = useYTWalletBalance(ytToken);
|
||||
const { formattedPrice } = useYTPrice(ytToken);
|
||||
const collateralUsd = (parseFloat(formattedBalance) * parseFloat(formattedPrice)).toFixed(2);
|
||||
|
||||
// Deposit hook
|
||||
const {
|
||||
status: depositStatus,
|
||||
error: depositError,
|
||||
isLoading: isDepositing,
|
||||
approveHash: depositApproveHash,
|
||||
supplyHash,
|
||||
executeApproveAndSupply,
|
||||
reset: resetDeposit,
|
||||
} = useLendingCollateral();
|
||||
|
||||
// Withdraw hook
|
||||
const {
|
||||
status: withdrawStatus,
|
||||
error: withdrawError,
|
||||
isLoading: isWithdrawing,
|
||||
withdrawHash,
|
||||
executeWithdrawCollateral,
|
||||
reset: resetWithdraw,
|
||||
} = useWithdrawCollateral();
|
||||
|
||||
const DEPOSIT_TOAST_ID = 'collateral-deposit-tx';
|
||||
const WITHDRAW_TOAST_ID = 'collateral-withdraw-tx';
|
||||
|
||||
// Deposit approve 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (depositApproveHash && depositStatus === 'approving') {
|
||||
toast.loading(t("repay.toast.approvalSubmitted"), {
|
||||
id: DEPOSIT_TOAST_ID,
|
||||
description: t("repay.toast.waitingConfirmation"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(depositApproveHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [depositApproveHash, depositStatus]);
|
||||
|
||||
// Deposit 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (supplyHash && depositStatus === 'supplying') {
|
||||
toast.loading(t("repay.toast.depositSubmitted"), {
|
||||
id: DEPOSIT_TOAST_ID,
|
||||
description: t("repay.toast.depositingNow"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(supplyHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [supplyHash, depositStatus]);
|
||||
|
||||
// Withdraw 交易提交 toast
|
||||
useEffect(() => {
|
||||
if (withdrawHash && withdrawStatus === 'withdrawing') {
|
||||
toast.loading(t("repay.toast.withdrawSubmitted"), {
|
||||
id: WITHDRAW_TOAST_ID,
|
||||
description: t("repay.toast.withdrawingNow"),
|
||||
action: { label: t("repay.toast.viewTx"), onClick: () => window.open(getTxUrl(withdrawHash), '_blank') },
|
||||
});
|
||||
}
|
||||
}, [withdrawHash, withdrawStatus]);
|
||||
|
||||
// Notify backend after successful tx
|
||||
useEffect(() => {
|
||||
if (depositStatus === 'success' && supplyHash && amount) {
|
||||
toast.success(t("repay.toast.depositSuccess"), {
|
||||
id: DEPOSIT_TOAST_ID,
|
||||
description: t("repay.toast.depositSuccessDesc"),
|
||||
duration: 5000,
|
||||
});
|
||||
notifySupplyCollateral(tokenType, amount, supplyHash);
|
||||
refetchBalance();
|
||||
refetchWalletBalance();
|
||||
onCollateralChanged?.();
|
||||
setAmount('');
|
||||
setTimeout(resetDeposit, 3000);
|
||||
}
|
||||
}, [depositStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (withdrawStatus === 'success' && withdrawHash && amount) {
|
||||
toast.success(t("repay.toast.withdrawSuccess"), {
|
||||
id: WITHDRAW_TOAST_ID,
|
||||
description: t("repay.toast.withdrawSuccessDesc"),
|
||||
duration: 5000,
|
||||
});
|
||||
notifyWithdrawCollateral(tokenType, amount, withdrawHash);
|
||||
refetchBalance();
|
||||
setAmount('');
|
||||
setTimeout(resetWithdraw, 3000);
|
||||
}
|
||||
}, [withdrawStatus]);
|
||||
|
||||
// 错误提示
|
||||
useEffect(() => {
|
||||
if (depositError) {
|
||||
if (depositError === 'Transaction cancelled') {
|
||||
toast.dismiss(DEPOSIT_TOAST_ID);
|
||||
} else {
|
||||
toast.error(t("repay.toast.depositFailed"), { id: DEPOSIT_TOAST_ID, duration: 5000 });
|
||||
}
|
||||
}
|
||||
}, [depositError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (withdrawError) {
|
||||
if (withdrawError === 'Transaction cancelled') {
|
||||
toast.dismiss(WITHDRAW_TOAST_ID);
|
||||
} else {
|
||||
toast.error(t("repay.toast.withdrawFailed"), { id: WITHDRAW_TOAST_ID, duration: 5000 });
|
||||
}
|
||||
}
|
||||
}, [withdrawError]);
|
||||
|
||||
const inputDecimals = ytToken?.onChainDecimals ?? ytToken?.decimals ?? 18;
|
||||
const displayDecimals = Math.min(inputDecimals, 6);
|
||||
const truncateDecimals = (s: string, d: number) => { const p = s.split('.'); return p.length > 1 ? `${p[0]}.${p[1].slice(0, d)}` : s; };
|
||||
const isValidAmount = (v: string) => v !== '' && !isNaN(parseFloat(v)) && parseFloat(v) > 0;
|
||||
|
||||
const handleAmountChange = (value: string) => {
|
||||
if (value === '') { setAmount(value); return; }
|
||||
if (!/^\d*\.?\d*$/.test(value)) return;
|
||||
const parts = value.split('.');
|
||||
if (parts.length > 1 && parts[1].length > displayDecimals) return;
|
||||
const maxBalance = activeTab === 'deposit' ? parseFloat(walletBalance) : parseFloat(formattedBalance);
|
||||
if (parseFloat(value) > maxBalance) {
|
||||
setAmount(truncateDecimals(activeTab === 'deposit' ? walletBalance : formattedBalance, displayDecimals));
|
||||
return;
|
||||
}
|
||||
setAmount(value);
|
||||
};
|
||||
|
||||
const handleAction = () => {
|
||||
if (!amount || parseFloat(amount) <= 0 || !ytToken) return;
|
||||
if (activeTab === 'deposit') {
|
||||
executeApproveAndSupply(ytToken, amount);
|
||||
} else {
|
||||
executeWithdrawCollateral(ytToken, amount);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMax = () => {
|
||||
if (activeTab === 'deposit') {
|
||||
setAmount(truncateDecimals(walletBalance, displayDecimals));
|
||||
} else {
|
||||
setAmount(truncateDecimals(formattedBalance, displayDecimals));
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isDepositing || isWithdrawing;
|
||||
const currentStatus = activeTab === 'deposit' ? depositStatus : withdrawStatus;
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (!isConnected) return t("common.connectWallet");
|
||||
if (currentStatus === 'approving') return t("common.approving");
|
||||
if (currentStatus === 'approved') return t("repay.confirmedDepositing");
|
||||
if (currentStatus === 'supplying') return t("repay.depositing");
|
||||
if (currentStatus === 'withdrawing') return t("repay.withdrawing");
|
||||
if (currentStatus === 'success') return t("common.success");
|
||||
if (currentStatus === 'error') return t("common.failed");
|
||||
if (amount && !isValidAmount(amount)) return t("common.invalidAmount");
|
||||
return activeTab === 'deposit' ? t("repay.deposit") : t("repay.withdraw");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-6 flex-1 shadow-md">
|
||||
{/* Title */}
|
||||
<h3 className="text-body-large font-bold text-text-primary dark:text-white leading-[150%]">
|
||||
{t("repay.supplyCollateral")}
|
||||
</h3>
|
||||
|
||||
{/* Token Info and Value */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-bold shadow-md overflow-hidden flex-shrink-0"
|
||||
style={{ background: GRADIENT_COLORS[tokenType] ?? DEFAULT_GRADIENT }}
|
||||
>
|
||||
{ytToken?.iconUrl ? (
|
||||
<Image src={ytToken.iconUrl} alt={tokenType} width={40} height={40} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
tokenType.replace('YT-', '')
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-heading-h4 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em] truncate">
|
||||
{!mounted ? '--' : parseFloat(formattedBalance).toLocaleString('en-US', { maximumFractionDigits: displayDecimals })}
|
||||
</span>
|
||||
<span className="text-body-small font-bold text-text-tertiary dark:text-gray-400 flex-shrink-0">{tokenType}</span>
|
||||
</div>
|
||||
<span className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
||||
{!mounted ? '--' : `$${parseFloat(collateralUsd).toLocaleString('en-US', { maximumFractionDigits: 2 })}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex rounded-xl bg-fill-secondary-click dark:bg-gray-700 p-1">
|
||||
<button
|
||||
className={`flex-1 rounded-lg py-2 text-body-small font-bold transition-all ${
|
||||
activeTab === 'deposit'
|
||||
? 'bg-white dark:bg-gray-600 text-text-primary dark:text-white shadow-sm'
|
||||
: 'text-text-tertiary dark:text-gray-400'
|
||||
}`}
|
||||
onClick={() => { setActiveTab('deposit'); setAmount(''); resetDeposit(); }}
|
||||
>
|
||||
{t("repay.deposit")}
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 rounded-lg py-2 text-body-small font-bold transition-all ${
|
||||
activeTab === 'withdraw'
|
||||
? 'bg-white dark:bg-gray-600 text-text-primary dark:text-white shadow-sm'
|
||||
: 'text-text-tertiary dark:text-gray-400'
|
||||
}`}
|
||||
onClick={() => { setActiveTab('withdraw'); setAmount(''); resetWithdraw(); }}
|
||||
>
|
||||
{t("repay.withdraw")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-caption-tiny font-medium text-text-tertiary dark:text-gray-400">
|
||||
{t("repay.amount")}
|
||||
</span>
|
||||
{activeTab === 'deposit' && mounted && (
|
||||
<button
|
||||
onClick={handleMax}
|
||||
className="text-caption-tiny font-bold text-[#ff6900] hover:opacity-80"
|
||||
>
|
||||
{t("common.max")}: {parseFloat(walletBalance).toLocaleString('en-US', { maximumFractionDigits: displayDecimals })} {tokenType}
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'withdraw' && mounted && (
|
||||
<button
|
||||
onClick={handleMax}
|
||||
className="text-caption-tiny font-bold text-[#ff6900] hover:opacity-80"
|
||||
>
|
||||
{t("common.max")}: {parseFloat(formattedBalance).toLocaleString('en-US', { maximumFractionDigits: displayDecimals })} {tokenType}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center bg-bg-subtle dark:bg-gray-700 rounded-xl px-4 h-[52px] gap-2">
|
||||
<input
|
||||
type="text" inputMode="decimal"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => handleAmountChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-body-default font-bold text-text-primary dark:text-white outline-none"
|
||||
/>
|
||||
<span className="text-body-small font-bold text-text-tertiary dark:text-gray-400">
|
||||
{tokenType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<Button
|
||||
isDisabled={!mounted || (isConnected && (!isValidAmount(amount) || isLoading || currentStatus === 'success'))}
|
||||
color="default"
|
||||
className={buttonStyles({ intent: "theme" })}
|
||||
onPress={() => !isConnected ? open() : handleAction()}
|
||||
>
|
||||
{!mounted ? 'Loading...' : getButtonLabel()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user