From 594677498670286cf42b7129fc0a7bb2d24606ae Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sat, 10 Jan 2026 13:01:07 +0530 Subject: [PATCH 01/65] improved teh handleleverage button handling --- .../marketplace/asset/AssetDetails.page.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/pages/marketplace/asset/AssetDetails.page.tsx b/src/pages/marketplace/asset/AssetDetails.page.tsx index 5bb6114..a40c4f3 100644 --- a/src/pages/marketplace/asset/AssetDetails.page.tsx +++ b/src/pages/marketplace/asset/AssetDetails.page.tsx @@ -16,6 +16,7 @@ import { AreaChart, XAxis, YAxis, Tooltip, ResponsiveContainer, Area } from 'rec import { useLeverageStore } from '../../../stores/leverage.store'; import { parseUnits } from 'viem'; import { LEVERAGE_CONTRACTS, METH_ABI } from '../../../lib/blockchain/leverage.contract'; +import { PageLoader } from '../../../components/ui/page-loader'; // USDC Contract Address const USDC_ADDRESS = (import.meta.env.VITE_USDC_ADDRESS || '0x9A54Bad93a00Bf1232D4e636f5e53055Dc0b8238') as `0x${string}`; @@ -261,7 +262,7 @@ const AssetDetailsPage = () => { console.log('Token Address:', asset.token?.address || 'N/A'); console.log('Buyer Address:', address); console.log('==============================================\n'); - + setIsPurchasing(true); setLeveragePurchaseStatus(null); try { @@ -354,11 +355,13 @@ const AssetDetailsPage = () => { setIsApproving(false); // Reset form and reload wallet data setLeverageTokenInput(''); + setIsPurchasing(false); await loadWalletData(); // Briefly show success then reset button state setTimeout(() => { setLeveragePurchaseStatus(null); + setIsPurchasing(false); }, 2500); console.log('\n===== LEVERAGED PURCHASE FLOW FAILED =====\n'); return; @@ -469,6 +472,7 @@ const AssetDetailsPage = () => { // Briefly show success then reset button state setTimeout(() => { setLeveragePurchaseStatus(null); + setIsPurchasing(false); }, 2500); console.log('\n✨ ===== LEVERAGED PURCHASE COMPLETED =====\n'); @@ -479,6 +483,7 @@ const AssetDetailsPage = () => { // Reset status after showing failure briefly setTimeout(() => { setLeveragePurchaseStatus(null); + setIsPurchasing(false); }, 3000); console.log('\n===== LEVERAGED PURCHASE FLOW FAILED =====\n'); } @@ -489,7 +494,9 @@ const AssetDetailsPage = () => { : true; if (isLoadingAsset) { - return
Loading...
; + return
+ +
; } if (error) { @@ -647,11 +654,15 @@ const AssetDetailsPage = () => { Invoice {asset.metadata.invoiceNumber}
+

Status: {asset.status}

+ {/* Chart Section */} @@ -735,6 +746,7 @@ const AssetDetailsPage = () => {

{tokens} Tokens

{props.payload.purchaseMethod && (

+ Method: {props.payload.purchaseMethod} @@ -1035,8 +1047,8 @@ const AssetDetailsPage = () => { - ))} + ); + })} )} diff --git a/src/types/issuer.types.ts b/src/types/issuer.types.ts index f5ad1f4..35e3841 100644 --- a/src/types/issuer.types.ts +++ b/src/types/issuer.types.ts @@ -69,6 +69,8 @@ export interface AssetMetadata { buyerName: string; industry: string; riskTier: string; + name?: string; + image?: string; } export interface TokenParams { From 6e7c3e9ee92cb42490b019cf0c0f9a6c92dde8d9 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sat, 10 Jan 2026 15:17:58 +0530 Subject: [PATCH 05/65] feat: enhance TradesTable with asset integration and order cancellation dialog --- src/components/portfolio/TradesTable.tsx | 542 ++++++++++++--------- src/hooks/useAuctionContracts.ts | 4 +- src/pages/marketplace/Marketplace.page.tsx | 5 +- src/pages/portfolio/Portfolio.page.tsx | 9 +- 4 files changed, 308 insertions(+), 252 deletions(-) diff --git a/src/components/portfolio/TradesTable.tsx b/src/components/portfolio/TradesTable.tsx index c5fc31b..119fd91 100644 --- a/src/components/portfolio/TradesTable.tsx +++ b/src/components/portfolio/TradesTable.tsx @@ -2,9 +2,20 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ExternalLink, ChevronDown, ChevronUp, Filter, XCircle } from 'lucide-react'; import type { SecondaryOrder } from '../../types/marketplace.types'; +import type { PortfolioAsset } from '@/lib/api/portfolio.service'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '../ui/dialog'; +import { Button } from '../ui/button'; interface TradesTableProps { orders: SecondaryOrder[]; + assets: PortfolioAsset[]; isLoading: boolean; onCancelOrder: (orderId: string) => void; isCancellingId: string | null; @@ -12,6 +23,7 @@ interface TradesTableProps { export const TradesTable = ({ orders, + assets, isLoading, onCancelOrder, isCancellingId, @@ -20,6 +32,8 @@ export const TradesTable = ({ const [expandedRows, setExpandedRows] = useState>(new Set()); const [statusFilter, setStatusFilter] = useState<'ALL' | 'OPEN' | 'FILLED' | 'CANCELLED'>('ALL'); const [typeFilter, setTypeFilter] = useState<'ALL' | 'BUY' | 'SELL'>('ALL'); + const [orderToCancel, setOrderToCancel] = useState(null); + // const [hoveredRow, setHoveredRow] = useState(null); // Filter orders based on selected filters const filteredOrders = orders.filter(order => { @@ -31,6 +45,11 @@ export const TradesTable = ({ const statusOptions: Array<'ALL' | 'OPEN' | 'FILLED' | 'CANCELLED'> = ['ALL', 'OPEN', 'FILLED', 'CANCELLED']; const typeOptions: Array<'ALL' | 'BUY' | 'SELL'> = ['ALL', 'BUY', 'SELL']; + const getRowKey = (order: SecondaryOrder): string => { + return order._id; + + }; + const toggleRowExpansion = (key: string, e: React.MouseEvent) => { e.stopPropagation(); const newExpanded = new Set(expandedRows); @@ -42,6 +61,13 @@ export const TradesTable = ({ setExpandedRows(newExpanded); }; + const handleConfirmCancel = () => { + if (orderToCancel) { + onCancelOrder(orderToCancel); + setOrderToCancel(null); + } + }; + const formatTokenAmount = (weiAmount: string): string => { const tokens = parseFloat(weiAmount) / 1e18; return tokens.toLocaleString('en-US', { maximumFractionDigits: 4 }); @@ -96,269 +122,299 @@ export const TradesTable = ({ } return ( -

- {/* Filter Bar */} -
- {/* Type Filter */} -
- - Type: -
- {typeOptions.map((type) => ( - - ))} + <> +
+ {/* Filter Bar */} +
+ {/* Type Filter */} +
+ + Type: +
+ {typeOptions.map((type) => ( + + ))} +
-
- {/* Status Filter */} -
- - Status: -
- {statusOptions.map((status) => ( - - ))} + {/* Status Filter */} +
+ + Status: +
+ {statusOptions.map((status) => ( + + ))} +
+ {(statusFilter !== 'ALL' || typeFilter !== 'ALL') && ( + + ({filteredOrders.length} of {orders.length}) + + )}
- {(statusFilter !== 'ALL' || typeFilter !== 'ALL') && ( - - ({filteredOrders.length} of {orders.length}) - - )}
-
- - - - - )} - - ); - })} - -
+ + + + + + + + + + + + + + + + {filteredOrders.map((order, index) => { + const rowKey = getRowKey(order); + const isExpanded = expandedRows.has(order._id); + const totalValue = (parseFloat(order.initialAmount) / 1e18) * (parseFloat(order.pricePerToken) / 1e6); - - - - - - - - - - - - - {filteredOrders.map((order, index) => { - const isExpanded = expandedRows.has(order._id); - const totalValue = (parseFloat(order.initialAmount) / 1e18) * (parseFloat(order.pricePerToken) / 1e6); - - return ( - <> - - {/* Expand Icon */} - - - {/* Type */} - - - {/* Asset ID / Order ID */} - toggleRowExpansion(rowKey, e)} + > + {/* Expand Icon */} + + + {/* Type */} + + + {/* Asset ID / Order ID */} + - - {/* Status */} - - - {/* Amount */} - + + {/* Status */} + + + {/* Amount */} + - - {/* Price */} - - - {/* Total Value */} - - - {/* Date */} - - - {/* Actions */} - - - - {/* Expanded Row Details */} - {isExpanded && ( - - + + {/* Price */} + + + {/* Total Value */} + + + {/* Date */} + + + {/* Actions */} + + + + {/* Expanded Row Details */} + {isExpanded && ( + + + + )} + + ); + })} + +
+ + + Type + + Asset ID / Order ID + + Status + + Amount (Tokens) + + Price (USDC) + + Total Value + + Date + + Actions +
- Type - - Asset ID / Order ID - - Status - - Amount (Tokens) - - Price (USDC) - - Total Value - - Date - - Actions -
- - - - {order.isBuy ? 'BUY' : 'SELL'} - - -
-
{ navigate(`/marketplace/asset/${order.assetId}`) }}> - {order.assetId.slice(0, 8)}...{order.assetId.slice(-4)} -
-
- Order #{order.orderId} + + return ( + <> +
+ + + + {order.isBuy ? 'BUY' : 'SELL'} + + +
+
+ {assets.find(a => a.assetId === order.assetId) && ( + { navigate(`/marketplace/asset/${order.assetId}`) }}> + {assets.find(a => a.assetId === order.assetId)?.metadata?.assetName || 'Unknown Asset'} + + )} + + {order.assetId.slice(0, 8)}...{order.assetId.slice(-4)} + +
+
{ navigate(`/trade/asset/${order.assetId}`) }}> + Order #{order.orderId} +
- -
- - {order.status} - - -
-
+
+ + {order.status} + + +
+
Initial: {formatTokenAmount(order.initialAmount)} Remaining: {formatTokenAmount(order.remainingAmount)}
-
-
-
- ${formatUSDCAmount(order.pricePerToken)} -
-
-
- ${totalValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
-
-
- {new Date(order.createdAt).toLocaleDateString()} -
-
-
- {order.status === 'OPEN' && ( - - )} - - {/* Transaction Hash Links */} - {order.txHash && ( - - )} -
-
- {/* Basic Details Grid */} -
- {/* Token Address */} -
-
Token Address
-
- {order.tokenAddress ? truncateAddress(order.tokenAddress) : 'N/A'} +
+
+
+ ${formatUSDCAmount(order.pricePerToken)} +
+
+
+ ${totalValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+
+ {new Date(order.createdAt).toLocaleDateString()} +
+
+
+ {order.status === 'OPEN' && ( + + )} + + {/* Transaction Hash Links */} + {order.txHash && ( + + )} +
+
+ {/* Basic Details Grid */} +
+ {/* Token Address */} +
+
Token Address
+
+ {order.tokenAddress ? truncateAddress(order.tokenAddress) : 'N/A'} +
-
- {/* Maker Address */} -
-
Maker Address
-
- {order.maker ? truncateAddress(order.maker) : 'N/A'} + {/* Maker Address */} +
+
Maker Address
+
+ {order.maker ? truncateAddress(order.maker) : 'N/A'} +
-
- {/* Created At */} -
-
Created At
-
- {new Date(order.createdAt).toLocaleString()} + {/* Created At */} +
+
Created At
+
+ {new Date(order.createdAt).toLocaleString()} +
-
- {/* Updated At */} -
-
Updated At
-
- {new Date(order.updatedAt).toLocaleString()} + {/* Updated At */} +
+
Updated At
+
+ {new Date(order.updatedAt).toLocaleString()} +
-
- - {/* Transaction Hash */} -
+ + {/* Transaction Hash */} +
Transaction Hash
{order.txHash}
+
+
+
+ - - -
-
- ); -}; + !open && setOrderToCancel(null)}> + + + Confirm Order Cancellation + + This will cancel the trade order and make the remaining tokens available in your portfolio. Are you sure you want to proceed? + + + + + + + + + +)} \ No newline at end of file diff --git a/src/hooks/useAuctionContracts.ts b/src/hooks/useAuctionContracts.ts index d7b4923..ff98d85 100644 --- a/src/hooks/useAuctionContracts.ts +++ b/src/hooks/useAuctionContracts.ts @@ -575,10 +575,10 @@ export function useSettleBid() { console.log('✅ Backend notified successfully!'); - // Refresh page after 2 seconds to show updated portfolio + // Refresh page after 1 second to show updated portfolio setTimeout(() => { window.location.reload(); - }, 2000); + }, 1000); } catch (error: any) { console.error('❌ Backend notification failed:', error); setError(`Settlement complete, but backend notification failed: ${error.message}`); diff --git a/src/pages/marketplace/Marketplace.page.tsx b/src/pages/marketplace/Marketplace.page.tsx index 6b90c82..3af160b 100644 --- a/src/pages/marketplace/Marketplace.page.tsx +++ b/src/pages/marketplace/Marketplace.page.tsx @@ -306,7 +306,6 @@ const MarketplacePage = () => { const filters: { value: FilterCategory; label: string }[] = [ { value: 'all', label: 'All Assets' }, { value: 'invoices', label: 'Invoices' }, - { value: 'high-yield', label: 'High Yield (>10%)' }, { value: 'short-term', label: 'Short Term (<6mo)' }, { value: 'verified', label: 'Verified' }, @@ -318,7 +317,7 @@ const MarketplacePage = () => { const numDays = typeof days === 'string' ? parseFloat(days) : days; if (numDays < 30) return `${numDays} days`; if (numDays < 365) return `${Math.floor(numDays / 30)} months`; - return `Matured`; + return `Payout Requested`; }; // Calculate time remaining for auction @@ -1080,7 +1079,7 @@ const MarketplacePage = () => { (typeof asset.maturityDays === 'number' && asset.maturityDays <= 0) || asset.fundingProgress === 100; - return isMatured ? ( + return isMatured && asset.status === 'PAYOUT_COMPLETE' ? ( - - {/* Right: Wallet Display */} -
{address && ( <> @@ -693,6 +693,7 @@ const PortfolioPage = () => { >
Date: Sat, 10 Jan 2026 15:37:09 +0530 Subject: [PATCH 06/65] graceful error showing --- .../borrow/components/UnifiedBorrowModal.tsx | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index 7733517..f2f83a3 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -35,6 +35,16 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData }: BorrowOnly } }, [isOpen, positions, selectedPosition]); + + useEffect(() => { + if (!error) return; + + const timer = setTimeout(() => { + setError(null); + }, 5000); // ⏱ 5 seconds + + return () => clearTimeout(timer); + }, [error]); // Fetch asset details for all positions useEffect(() => { const fetchAllAssetDetails = async () => { @@ -291,14 +301,25 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData }: BorrowOnly
{installmentError && ( -
{installmentError}
+
{installmentError}
)}
{/* Error Banner */} {error && ( -
-

{error}

+
+

+ {(() => { + // Extract revert reason from error message + const reasonMatch = error.match(/reason="([^"]+)"/); + if (reasonMatch && reasonMatch[1]) { + return reasonMatch[1]; + } + + // Fallback to showing the full error if no reason found + return error; + })()} +

)} From 2b03f55cbf11d9f96e1f27e62be2bc8b81d14aa7 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sat, 10 Jan 2026 15:40:05 +0530 Subject: [PATCH 07/65] feat: implement polling for orderbook updates and adjust loading states --- .../TradingEngine.page.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pages/secondary-marketplace/TradingEngine.page.tsx b/src/pages/secondary-marketplace/TradingEngine.page.tsx index 47434af..87bb23b 100644 --- a/src/pages/secondary-marketplace/TradingEngine.page.tsx +++ b/src/pages/secondary-marketplace/TradingEngine.page.tsx @@ -70,6 +70,7 @@ const TradingEngineProductionPage = () => { const { address, isConnected } = useAccount(); const { toasts, success, error: showError, warning, removeToast } = useToast(); const navigate = useNavigate(); + const [polling, setPolling] = useState(false); // Store State const { @@ -188,9 +189,9 @@ const TradingEngineProductionPage = () => { if (!assetId) return; fetchAssetDetails(assetId); fetchOrderbook(assetId); - fetchTradeHistory(assetId); + // fetchTradeHistory(assetId); if (address) { - fetchMyOrders(assetId); + // fetchMyOrders(assetId); fetchTradeableBalance(assetId); } @@ -211,6 +212,15 @@ const TradingEngineProductionPage = () => { fetchPurchaseData(); }, [assetId, address, fetchAssetDetails, fetchOrderbook, fetchTradeHistory, fetchMyOrders, fetchTradeableBalance]); + + useEffect(() => { + if(!assetId) return; + fetchOrderbook(assetId); + setPolling(true); + const poll = setInterval(() => { fetchOrderbook(assetId); setPolling(true); }, 2000); + return () => { clearInterval(poll); setPolling(false); }; + }, [fetchOrderbook]); + // Aggregate purchase data into time blocks const aggregateIntoTimeBlocks = (chartData: any[], intervalMinutes: number = 0.05) => { if (!chartData || chartData.length === 0) return []; @@ -541,7 +551,7 @@ const TradingEngineProductionPage = () => { {/* MARKET DEPTH (Orderbook) */}
- {isLoadingOrderbook ? ( + {isLoadingOrderbook && !polling ? (
@@ -825,7 +835,7 @@ const TradingEngineProductionPage = () => {
{/* Transaction Progress */} - {(isTxPending || isTxConfirming) && ( + {/* {(isTxPending || isTxConfirming) && (
@@ -835,7 +845,7 @@ const TradingEngineProductionPage = () => {

- )} + )} */} {/* Execute Button */}
@@ -129,12 +116,10 @@ export const ToastContainer = ({ toasts, onClose }: ToastContainerProps) => { if (toasts.length === 0) return null; return ( -
-
- {toasts.map((toast) => ( - - ))} -
+
+ {toasts.map((toast) => ( + + ))}
); }; From e5e40e04b98db4035984bce359ef4483a50d2afa Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sat, 10 Jan 2026 15:59:21 +0530 Subject: [PATCH 09/65] updated the leverage & portfolio demo links --- src/components/ui/faq-details.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/faq-details.tsx b/src/components/ui/faq-details.tsx index 0067d14..ad1dff6 100644 --- a/src/components/ui/faq-details.tsx +++ b/src/components/ui/faq-details.tsx @@ -106,7 +106,7 @@ const FaqDetails = ({ description: "Use mETH as collateral to create leveraged exposure while yield services interest automatically.", buttonText: "Use mETH", - videoSrc: "", + videoSrc: "https://youtu.be/c7CdBkcVZPQ", }, }, { @@ -119,7 +119,7 @@ const FaqDetails = ({ description: "View owned tokens, auction bids, leveraged positions, loans, yield history, and health metrics.", buttonText: "View Portfolio", - videoSrc: "", + videoSrc: "https://youtu.be/NtLOZqsX5U8", }, }, { From b44613d4223b61935b15ffa106f3900913b71ce7 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sat, 10 Jan 2026 16:53:11 +0530 Subject: [PATCH 10/65] fixed routing and prop issue --- src/app/router/public.routes.tsx | 5 +++++ src/pages/borrow/components/UnifiedBorrowModal.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/router/public.routes.tsx b/src/app/router/public.routes.tsx index 71dd994..1a10fad 100644 --- a/src/app/router/public.routes.tsx +++ b/src/app/router/public.routes.tsx @@ -36,6 +36,7 @@ import { NotFoundPage } from '../../components/ui/404-page-not-found'; import AboutPage from '../../pages/landing/About.page'; import ChangelogPage from '../../pages/landing/Changelog.page'; import FAQSection from '../../pages/landing/FAQ.page'; +import BorrowPage from '../../pages/borrow/BorrowPage'; // Challenge Verification Page @@ -60,6 +61,10 @@ export const publicRoutes: RouteObject[] = [ path: '/marketplace', element: , }, + { + path :'/borrow', + element: , + }, { path: '/marketplace/asset/:assetId', element: , diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index 0a119de..65e01ee 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -10,11 +10,13 @@ import { formatCollateralAmount } from '../../../utils/solvency/formatters'; interface BorrowOnlyModalProps { isOpen: boolean; + onClose: () => void; + onSuccess: () => void; creditData: OAIDCreditLine | null; } -export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData }: BorrowOnlyModalProps) => { +export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: BorrowOnlyModalProps) => { const [borrowAmount, setBorrowAmount] = useState(''); const [selectedPosition, setSelectedPosition] = useState(null); const [showPositionSelector, setShowPositionSelector] = useState(false); From d459237fa6bfa0fc93edf24aa3321fbef38b6452 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sat, 10 Jan 2026 17:00:38 +0530 Subject: [PATCH 11/65] Refactor code structure for improved readability and maintainability --- src/components/faucet/TokenFaucetCard.tsx | 2 +- src/components/marketplace/SentimentChart.tsx | 15 +- src/components/marketplace/TradeChart.tsx | 30 +- src/components/portfolio/ActiveBidsTable.tsx | 4 +- src/components/portfolio/MiniAreaChart.tsx | 4 +- src/components/portfolio/MyAssetsTable.tsx | 16 +- src/components/portfolio/MyLoansTable.tsx | 20 +- src/components/portfolio/NoAssetsModal.tsx | 2 +- src/components/portfolio/PortfolioStats.tsx | 12 +- src/components/portfolio/RepayLoanModal.tsx | 2 +- src/components/portfolio/TradesTable.tsx | 8 +- src/components/ui/wavy.tsx | 2 +- src/pages/faucet/Faucet.page.tsx | 12 +- src/pages/portfolio/Portfolio.page.tsx | 28 +- .../TradingEngine.page.tsx | 1087 +++++++++-------- 15 files changed, 621 insertions(+), 623 deletions(-) diff --git a/src/components/faucet/TokenFaucetCard.tsx b/src/components/faucet/TokenFaucetCard.tsx index 0dece80..427c9d4 100644 --- a/src/components/faucet/TokenFaucetCard.tsx +++ b/src/components/faucet/TokenFaucetCard.tsx @@ -92,7 +92,7 @@ export const TokenFaucetCard = ({ }; return ( -
+
{/* Card Header */}
diff --git a/src/components/marketplace/SentimentChart.tsx b/src/components/marketplace/SentimentChart.tsx index d172956..fc23801 100644 --- a/src/components/marketplace/SentimentChart.tsx +++ b/src/components/marketplace/SentimentChart.tsx @@ -56,7 +56,7 @@ export const SentimentChart = ({ data, isLoading }: SentimentChartProps) => { return { backgroundColor: 'transparent', - animation: false, + animation: true, tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#111827' } }, @@ -127,6 +127,7 @@ export const SentimentChart = ({ data, isLoading }: SentimentChartProps) => { startValue: Math.min(0, data.length), endValue: data.length - 1, zoomLock: false // Set to true to maintain the "Industrial" constant candle width + } ], series: [ @@ -163,18 +164,16 @@ export const SentimentChart = ({ data, isLoading }: SentimentChartProps) => { }; return ( -
-
+
+

Market Sentiment

{isLoading && ( -
-
- -
+
+
)} -
+
); }; \ No newline at end of file diff --git a/src/components/marketplace/TradeChart.tsx b/src/components/marketplace/TradeChart.tsx index f19f8aa..3942be0 100644 --- a/src/components/marketplace/TradeChart.tsx +++ b/src/components/marketplace/TradeChart.tsx @@ -120,17 +120,6 @@ export const TradeChart = ({ data, isLoading }: TradeChartProps) => { endValue: data.length - 1, zoomLock: false }, - { - type: 'slider', - show: true, - xAxisIndex: [0], - height: 20, - bottom: 10, - borderColor: 'transparent', - backgroundColor: '#f8fafc', - fillerColor: 'rgba(51, 65, 85, 0.1)', - handleStyle: { color: '#cbd5e1' } - } ], series: [ { @@ -166,27 +155,18 @@ export const TradeChart = ({ data, isLoading }: TradeChartProps) => { }; return ( -
+

Trade Executions

-
-
-

5m Continuous Streaming

-
- {isLoading && ( -
-
- -
+ {isLoading && ( +
+
)} -
-
- Drag chart or use slider to explore historical trends -
+
); }; diff --git a/src/components/portfolio/ActiveBidsTable.tsx b/src/components/portfolio/ActiveBidsTable.tsx index ef7c49c..a2b5d18 100644 --- a/src/components/portfolio/ActiveBidsTable.tsx +++ b/src/components/portfolio/ActiveBidsTable.tsx @@ -79,7 +79,7 @@ export const ActiveBidsTable = ({ return (
{/* Filter Bar */} -
+
Filter by Status: @@ -106,7 +106,7 @@ export const ActiveBidsTable = ({
- +
Asset ID diff --git a/src/components/portfolio/MiniAreaChart.tsx b/src/components/portfolio/MiniAreaChart.tsx index 1ff1e6e..38bda8d 100644 --- a/src/components/portfolio/MiniAreaChart.tsx +++ b/src/components/portfolio/MiniAreaChart.tsx @@ -22,8 +22,8 @@ export const MiniAreaChart = ({ if (!data || data.length === 0) return null; return ( -
- +
+ {/* Filter Bar */} -
+
{/* Type Filter */}
@@ -242,8 +242,8 @@ export const MyAssetsTable = ({
- - + + @@ -283,7 +283,7 @@ export const MyAssetsTable = ({ <> setHoveredRow(rowKey)} onMouseLeave={() => setHoveredRow(null)} @@ -485,7 +485,7 @@ export const MyAssetsTable = ({ {/* Expanded Row Details */} {isExpanded && ( - +
{/* Basic Details Grid */}
@@ -603,7 +603,7 @@ export const MyAssetsTable = ({
- + @@ -617,9 +617,9 @@ export const MyAssetsTable = ({ - + {asset.transactionHistory.map((tx: any, txIndex: number) => ( - + diff --git a/src/pages/secondary-marketplace/TradingEngine.page.tsx b/src/pages/secondary-marketplace/TradingEngine.page.tsx index 41e058a..7255401 100644 --- a/src/pages/secondary-marketplace/TradingEngine.page.tsx +++ b/src/pages/secondary-marketplace/TradingEngine.page.tsx @@ -298,7 +298,7 @@ const TradingEngineProductionPage = () => { } }, [p2pError]); - const isTradeable = currentAsset?.status === 'LISTED' || currentAsset?.status === 'ACTIVE' || currentAsset?.status !== 'PAYOUT_COMPLETE'; + const isTradeable = currentAsset?.status === 'LISTED' || currentAsset?.status === 'ACTIVE' || false; // Calculation Helpers const getRequiredTokenApproval = useCallback(() => { diff --git a/src/types/marketplace.types.ts b/src/types/marketplace.types.ts index c5f55fb..39d017b 100644 --- a/src/types/marketplace.types.ts +++ b/src/types/marketplace.types.ts @@ -379,6 +379,7 @@ export interface SecondaryOrder { pricePerToken: string; // price in USDC (6 decimals) status: 'OPEN' | 'FILLED' | 'CANCELLED'; txHash: string; + stlTxHash: string; blockNumber: number; blockTimestamp: string; createdAt: string; From 0ea9dea2f63d53a309c9716b107721716b169095 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 19:01:35 +0530 Subject: [PATCH 24/65] added properly the validations for teh borrow feature --- src/components/portfolio/MyLoansTable.tsx | 195 +++++++++++++++--- src/lib/api/solvency-contract.service.ts | 40 +++- src/lib/api/solvency.service.ts | 47 +++++ .../components/DepositCollateralModal.tsx | 12 +- .../borrow/components/UnifiedBorrowModal.tsx | 157 +++++++++++--- 5 files changed, 393 insertions(+), 58 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 9930d5c..2c084d5 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -19,6 +19,7 @@ import { createPortal } from 'react-dom'; import { ChevronDown, ChevronUp, Wallet, Filter, Calendar, AlertCircle, Info, Clock, DollarSign } from 'lucide-react'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; +import { solvencyContractService } from '../../lib/api/solvency-contract.service'; import { format } from 'date-fns'; import { RepayLoanModal } from './RepayLoanModal'; @@ -119,7 +120,9 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr const [scheduleData, setScheduleData] = useState>({});; const [loadingSchedule, setLoadingSchedule] = useState>({}); const [showRepayModal, setShowRepayModal] = useState(false); + const [showWithdrawModal, setShowWithdrawModal] = useState(false); const [selectedPosition, setSelectedPosition] = useState(null); + const [withdrawingPositionId, setWithdrawingPositionId] = useState(null); // Filter positions const filteredPositions = useMemo(() => { @@ -187,6 +190,40 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr } }; + const handleWithdrawClick = (position: Position, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedPosition(position); + setShowWithdrawModal(true); + }; + + const handleWithdrawConfirm = async () => { + if (!selectedPosition) return; + + setWithdrawingPositionId(selectedPosition.positionId); + + try { + // Call smart contract directly to withdraw collateral + const amountBigInt = BigInt(selectedPosition.collateralAmount); + const result = await solvencyContractService.withdrawCollateral( + selectedPosition.positionId, + amountBigInt + ); + + if (result.success) { + setShowWithdrawModal(false); + setSelectedPosition(null); + if (onRefresh) onRefresh(); + } else { + throw new Error(result.error || 'Withdrawal failed'); + } + } catch (error: any) { + console.error('Withdrawal error:', error); + alert(`Withdrawal failed: ${error.message}`); + } finally { + setWithdrawingPositionId(null); + } + }; + if (isLoading) { return (
@@ -303,9 +340,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
- + @@ -325,6 +360,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr const isOverdue = isPaymentOverdue(position.nextPaymentDueDate); const outstandingDebt = parseFloat(getOutstandingDebt(position)); const hasDebt = outstandingDebt > 0; + const hasCollateral = parseFloat(position.collateralAmount) > 0; + const canWithdraw = hasCollateral && !position.oaidCreditIssued && !position.isDefaulted; return ( <> @@ -403,12 +440,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr - {/* Health Factor */} - + {/* Status */} @@ -628,6 +679,100 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr />, document.body )} + + {/* Withdraw Confirmation Modal */} + {selectedPosition && showWithdrawModal && createPortal( +
+
+ {/* Header */} +
+
+

+ Withdraw Collateral +

+

+ Confirm withdrawal from Position #{selectedPosition.positionId} +

+
+ {!withdrawingPositionId && ( + + )} +
+ + {/* Withdrawal Details */} +
+
+
+ {getTokenSymbol(selectedPosition.collateralTokenAddress)} + + {formatCollateralAmount(selectedPosition.collateralAmount, 18)} tokens + +
+
+ + Collateral Value + + + {formatUSD(selectedPosition.tokenValueUSD)} + +
+
+ +
+
+ + + +

+ Your collateral will be returned to your wallet. Please confirm the transaction in your wallet. +

+
+
+
+ + {/* Processing State */} + {withdrawingPositionId === selectedPosition.positionId && ( +
+
+

Processing withdrawal...

+

Please confirm in your wallet

+
+ )} + + {/* Action Buttons */} + {!withdrawingPositionId && ( +
+ + +
+ )} +
+
, + document.body + )} ); }; diff --git a/src/lib/api/solvency-contract.service.ts b/src/lib/api/solvency-contract.service.ts index 1d58e47..25569b8 100644 --- a/src/lib/api/solvency-contract.service.ts +++ b/src/lib/api/solvency-contract.service.ts @@ -9,8 +9,8 @@ import { ethers } from 'ethers'; // Contract addresses from environment -const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x3b3d70Fe12076f30E9999Fd65feC6C6DeB47B5eF'; -const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x26Da2F1a2de3295302Fd95eBA1A183dc8Ffd77a3'; +const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_SOLVENCY_VAULT || '0x9c276B10456aCF5FD74A0f110242bBEC5287fCFc'; +const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x9A54Bad93a00Bf1232D4e636f5e53055Dc0b8238'; // Solvency Vault ABI - ✅ VERIFIED from deposit-to-vaultsolvency.js lines 115-121 // Updated borrowUSDC to include loanDuration and numberOfInstallments per COMPLETE_LOAN.md @@ -23,6 +23,7 @@ const SOLVENCY_VAULT_ABI = [ // Read functions 'function positions(uint256) view returns (address user, address collateralToken, uint256 collateralAmount, uint256 usdcBorrowed, uint256 tokenValueUSD, uint256 createdAt, bool active, uint8 tokenType)', + 'function repaymentPlans(uint256) view returns (uint256 loanDuration, uint256 numberOfInstallments, uint256 installmentInterval, uint256 installmentsPaid, uint256 missedPayments, uint256 nextPaymentDue, bool isActive)', 'function seniorPool() view returns (address)', // Events @@ -76,6 +77,16 @@ export interface Position { tokenType: number; // 0 = RWA, 1 = PRIVATE_ASSET } +export interface RepaymentPlan { + loanDuration: bigint; + numberOfInstallments: bigint; + installmentInterval: bigint; + installmentsPaid: bigint; + missedPayments: bigint; + nextPaymentDue: bigint; + isActive: boolean; +} + class SolvencyContractService { /** * Get provider and signer from browser wallet @@ -283,6 +294,31 @@ class SolvencyContractService { } } + /** + * Get repayment plan for a position + * Used to check if position has an active loan + */ + async getRepaymentPlan(positionId: number): Promise { + try { + console.log(`📋 Fetching repayment plan for position ${positionId}...`); + const vault = await this.getVaultContract(); + const plan = await vault.repaymentPlans(positionId); + + return { + loanDuration: plan[0], + numberOfInstallments: plan[1], + installmentInterval: plan[2], + installmentsPaid: plan[3], + missedPayments: plan[4], + nextPaymentDue: plan[5], + isActive: plan[6], + }; + } catch (error: any) { + console.error('❌ Error getting repayment plan:', error); + return null; + } + } + // ============================================ // WRITE FUNCTIONS // ============================================ diff --git a/src/lib/api/solvency.service.ts b/src/lib/api/solvency.service.ts index 249ff9d..3444a3a 100644 --- a/src/lib/api/solvency.service.ts +++ b/src/lib/api/solvency.service.ts @@ -375,6 +375,53 @@ class SolvencyService extends BaseService { } } + /** + * Withdraw collateral from position (Backend API - not smart contract) + * + * ✅ ENDPOINT: POST /solvency/withdraw + * + * Backend handles: + * - Position verification + * - On-chain withdrawCollateral call + * - Database update + * - Private asset collateral tracking update + */ + async withdrawCollateral(request: { + positionId: string; + amount: string; // Amount in wei (18 decimals) + }): Promise<{ + success: boolean; + txHash?: string; + blockNumber?: number; + position: Position; + }> { + try { + console.log('💰 Initiating collateral withdrawal:', request); + + const response = await this.fetchWithTimeout( + `${this.baseURL}/solvency/withdraw`, + { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify(request), + }, + 120000 // 2 minute timeout for blockchain tx + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to withdraw collateral'); + } + + const data = await response.json(); + console.log('✅ Withdrawal successful:', data); + return data; + } catch (error: any) { + console.error('❌ Error withdrawing collateral:', error); + throw error; + } + } + // ============================================ // ADMIN ENDPOINTS // ============================================ diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 68bd15f..d909aec 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -15,6 +15,7 @@ import { assetService } from '../../../lib/api/asset.service'; import { solvencyContractService } from '../../../lib/api/solvency-contract.service'; import { solvencyService } from '../../../lib/api/solvency.service'; import type { IssuerAsset } from '../../../types/issuer.types'; +import { marketplaceService } from '../../../lib/api/marketplace.service'; interface DepositCollateralModalProps { isOpen: boolean; @@ -264,8 +265,17 @@ export const DepositCollateralModal = ({ setStep('syncing'); console.log('🔄 Step 3/3: Syncing with platform...'); + // Sync portfolio (notify purchase) + await marketplaceService.notifyPurchase({ + assetId: asset.assetId, + txHash: depositResult.txHash!, + amount: `-${depositAmount}`, + blockNumber: depositResult.blockNumber!.toString(), + }); + + // Sync position with solvency system await solvencyService.syncPosition({ - positionId: depositResult.positionId, + positionId: depositResult.positionId!.toString(), txHash: depositResult.txHash!, blockNumber: depositResult.blockNumber!, }); diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index 16707a2..613a3cc 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -26,15 +26,82 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: B const [error, setError] = useState(null); const [selectedAsset, setSelectedAsset] = useState(null); const [assetDetailsMap, setAssetDetailsMap] = useState>({}); + const [positionValidation, setPositionValidation] = useState>({}); const availableCredit = creditData?.availableCredit ?? 0; const positions = creditData?.collateral ?? []; - // Auto-select first position + // Validate all positions on open useEffect(() => { - if (isOpen && positions.length > 0 && !selectedPosition) { - setSelectedPosition(positions[0]); - } + const validatePositions = async () => { + if (isOpen && positions.length > 0) { + const validationMap: Record = {}; + + await Promise.all(positions.map(async (position) => { + try { + // Check if position has a positionId + if (!position.positionId) { + validationMap[position.positionId ?? 0] = { valid: false, reason: 'Invalid position ID' }; + return; + } + + // Get position data from contract + const positionData = await solvencyContractService.getPosition(position.positionId); + + if (!positionData) { + validationMap[position.positionId] = { valid: false, reason: 'Position not found' }; + return; + } + + // Check if position is active + if (!positionData.active) { + validationMap[position.positionId] = { valid: false, reason: 'Position not active' }; + return; + } + + // Get repayment plan + const plan = await solvencyContractService.getRepaymentPlan(position.positionId); + + // Check if plan already exists and is active + if (plan && plan.isActive) { + validationMap[position.positionId] = { valid: false, reason: 'Active loan exists - must fully repay first' }; + return; + } + + // Get outstanding debt + const outstandingDebt = await solvencyContractService.getOutstandingDebt(position.positionId); + + // Calculate available credit + const ltv = positionData.tokenType === 0 ? 7000 : 6000; // RWA: 70%, PRIVATE_ASSET: 60% + const maxBorrow = (BigInt(positionData.tokenValueUSD) * BigInt(ltv)) / BigInt(10000); + const availableCredit = maxBorrow - outstandingDebt; + + if (availableCredit <= 0) { + validationMap[position.positionId] = { valid: false, reason: 'No available credit - LTV limit reached' }; + return; + } + + // Position is valid + validationMap[position.positionId] = { valid: true }; + } catch (err) { + console.error(`Failed to validate position ${position.positionId}:`, err); + validationMap[position.positionId ?? 0] = { valid: false, reason: 'Failed to validate position' }; + } + })); + + setPositionValidation(validationMap); + + // Auto-select first valid position + if (!selectedPosition) { + const firstValid = positions.find(p => validationMap[p.positionId ?? 0]?.valid); + if (firstValid) { + setSelectedPosition(firstValid); + } + } + } + }; + + validatePositions(); }, [isOpen, positions, selectedPosition]); @@ -402,36 +469,66 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: B const asset = assetDetailsMap[position.tokenAddress]; const name = asset?.metadata.invoiceNumber || position.tokenName; const symbol = asset?.metadata.invoiceNumber || position.tokenSymbol; - const industry = asset?.metadata.industry || position.tokenSymbol; + const industry = asset?.metadata.industry || position.tokenSymbol; + const validation = positionValidation[position.positionId ?? 0]; + const isValid = validation?.valid !== false; // Default to true if not yet validated + const reason = validation?.reason; return ( - + + + {/* Tooltip showing reason on hover */} + {!isValid && reason && ( +
+
+

Cannot borrow from this position

+

{reason}

+
+
+ )} + ); })} From 287fd38183f63f73772a2bc813f77064d91766e7 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 19:39:44 +0530 Subject: [PATCH 25/65] enhanced MyLoansTable with withdrawn positions tracking and updated RepayLoanModal for accurate debt display --- src/components/portfolio/MyLoansTable.tsx | 7 +++- src/components/portfolio/RepayLoanModal.tsx | 13 ++++-- src/lib/api/solvency-contract.service.ts | 40 ++++++++++++++----- .../borrow/components/UnifiedBorrowModal.tsx | 7 +--- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 2c084d5..86d23ce 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -123,6 +123,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr const [showWithdrawModal, setShowWithdrawModal] = useState(false); const [selectedPosition, setSelectedPosition] = useState(null); const [withdrawingPositionId, setWithdrawingPositionId] = useState(null); + const [withdrawnPositions, setWithdrawnPositions] = useState>(new Set()); // Filter positions const filteredPositions = useMemo(() => { @@ -210,6 +211,9 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr ); if (result.success) { + // Mark position as withdrawn + setWithdrawnPositions(prev => new Set([...prev, selectedPosition.positionId])); + setShowWithdrawModal(false); setSelectedPosition(null); if (onRefresh) onRefresh(); @@ -361,7 +365,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr const outstandingDebt = parseFloat(getOutstandingDebt(position)); const hasDebt = outstandingDebt > 0; const hasCollateral = parseFloat(position.collateralAmount) > 0; - const canWithdraw = hasCollateral && !position.oaidCreditIssued && !position.isDefaulted; + const wasWithdrawn = withdrawnPositions.has(position.positionId); + const canWithdraw = hasCollateral && !position.oaidCreditIssued && !position.isDefaulted && !wasWithdrawn; return ( <> diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index b263d0b..5c5f22f 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -282,9 +282,11 @@ export const RepayLoanModal = ({ Loading... ) : ( - - {formatUSD(outstandingDebt)} - + + {actualDebt + ? `$${ethers.formatUnits(actualDebt, 6)}` + : formatUSD(outstandingDebt)} + )} @@ -345,7 +347,10 @@ export const RepayLoanModal = ({ )} + {/* Collateral Section (Top - Like "Sell") */}
From 6e83882cd3121e24f192cc9190009bd2ab38a921 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sun, 11 Jan 2026 20:36:46 +0530 Subject: [PATCH 26/65] fix: update polling intervals in TradingEngine for accurate data fetching; refactor PortfolioStats and TradesTable components --- src/components/portfolio/PortfolioStats.tsx | 2 +- src/components/portfolio/TradesTable.tsx | 2 +- src/pages/admin/payout/PayoutView.page.tsx | 7 ++++++- src/pages/admin/settlements/SettlementView.page.tsx | 2 +- src/pages/borrow/BorrowPage.tsx | 2 +- src/pages/secondary-marketplace/TradingEngine.page.tsx | 4 ++-- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/portfolio/PortfolioStats.tsx b/src/components/portfolio/PortfolioStats.tsx index 3055b9c..8f32672 100644 --- a/src/components/portfolio/PortfolioStats.tsx +++ b/src/components/portfolio/PortfolioStats.tsx @@ -1,5 +1,5 @@ // src/components/portfolio/PortfolioStats.tsx -import { TrendingUp, Plus } from 'lucide-react'; +import { Plus } from 'lucide-react'; import { MiniAreaChart } from './MiniAreaChart'; import { generateAssetValueChart, generateYieldChart } from '../../lib/utils/portfolioChartGenerator'; import type { PortfolioAsset } from '../../lib/api/portfolio.service'; diff --git a/src/components/portfolio/TradesTable.tsx b/src/components/portfolio/TradesTable.tsx index f5ca620..8bcc8e3 100644 --- a/src/components/portfolio/TradesTable.tsx +++ b/src/components/portfolio/TradesTable.tsx @@ -340,7 +340,7 @@ export const TradesTable = ({ {order.stlTxHash && (
handleAssetSelect(asset)} - className={`cursor-pointer transition-all hover:bg-gray-200/70 ${ - selectedAsset?.assetId === asset.assetId ? 'bg-gray-300/50' : '' - }`} + className="cursor-pointer transition-all hover:bg-gray-200/70" > + @@ -388,15 +410,20 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {filteredPositions?.map((position, index) => { const isExpanded = expandedRows.has(position.positionId); + const pos = position as any; const schedule = scheduleData[position.positionId]; const isLoadingSchedule = loadingSchedule[position.positionId]; const isOverdue = isPaymentOverdue(position.nextPaymentDueDate); const outstandingDebt = parseFloat(getOutstandingDebt(position)); - const hasDebt =outstandingDebt > 0; + const hasDebt = outstandingDebt > 0; const hasCollateral = parseFloat(position.collateralAmount) > 0; const wasWithdrawn = withdrawnPositions.has(position.positionId); const canWithdraw = hasCollateral && !position.oaidCreditIssued && !position.isDefaulted && !wasWithdrawn; + // Use schedule from position if available, otherwise from fetched data + const displaySchedule = pos.repaymentSchedule || schedule?.installments; + const scheduleInfo = pos.repaymentSchedule ? pos : schedule; + return ( <> {position.collateralTokenType} @@ -472,6 +499,17 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr + {/* Status */} + + {/* Status */} @@ -545,76 +583,98 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {isExpanded && ( @@ -311,8 +322,18 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit > Settlement Tx - ) : ( - No Tx + ) : ( + liquidationTxHash ? ( + + Liquidation Tx + + ): + (No Tx) )} {status === 'SETTLED' && userYield && (
diff --git a/src/types/leverage.types.ts b/src/types/leverage.types.ts index f4efec3..8e70940 100644 --- a/src/types/leverage.types.ts +++ b/src/types/leverage.types.ts @@ -44,6 +44,7 @@ export interface LeveragePosition { settlementUSDCReceived?: string; userYieldDistributed?: string; mETHReturnedToUser?: string; + liquidationTxHash?: string; } export interface LeverageQuote { From 6b7b6a397f9e41a77d2a6bc3bcca9b7894869b49 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 02:10:15 +0530 Subject: [PATCH 53/65] fix: Reduce polling interval for chart data and order book fetching --- src/pages/secondary-marketplace/TradingEngine.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/secondary-marketplace/TradingEngine.page.tsx b/src/pages/secondary-marketplace/TradingEngine.page.tsx index 1e0e11d..53ca9bf 100644 --- a/src/pages/secondary-marketplace/TradingEngine.page.tsx +++ b/src/pages/secondary-marketplace/TradingEngine.page.tsx @@ -144,7 +144,7 @@ const TradingEngineProductionPage = () => { useEffect(() => { fetchChartData(); - const poll = setInterval(fetchChartData, 60000); + const poll = setInterval(fetchChartData, 30000); echarts.connect('trading-engine'); return () => clearInterval(poll); }, [fetchChartData]); @@ -218,7 +218,7 @@ const TradingEngineProductionPage = () => { if (!assetId) return; fetchOrderbook(assetId); setPolling(true); - const poll = setInterval(() => { fetchOrderbook(assetId); setPolling(true); }, 30000); + const poll = setInterval(() => { fetchOrderbook(assetId); setPolling(true); }, 4000); return () => { clearInterval(poll); setPolling(false); }; }, [fetchOrderbook]); From dbd45bf28a1b6bf8548d0c898ec9801069742ed3 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 10:57:15 +0530 Subject: [PATCH 54/65] refactor: Remove unused variables and imports in various components --- src/components/portfolio/MyAssetsTable.tsx | 3 --- src/components/portfolio/NoAssetsModal.tsx | 2 +- src/pages/admin/loans/LoansView.page.tsx | 14 -------------- src/pages/borrow/components/UnifiedBorrowModal.tsx | 2 +- src/pages/portfolio/Portfolio.page.tsx | 2 +- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/components/portfolio/MyAssetsTable.tsx b/src/components/portfolio/MyAssetsTable.tsx index 9db7071..189dbf9 100644 --- a/src/components/portfolio/MyAssetsTable.tsx +++ b/src/components/portfolio/MyAssetsTable.tsx @@ -94,7 +94,6 @@ export const MyAssetsTable = ({ claimStatus, }: MyAssetsTableProps) => { const navigate = useNavigate(); - const [hoveredRow, setHoveredRow] = useState(null); const [expandedRows, setExpandedRows] = useState>(new Set()); const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'SETTLED' | 'CONFIRMED' | 'CLAIMED'>('ALL'); const [typeFilter, setTypeFilter] = useState<'ALL' | 'STATIC' | 'LEVERAGE'>('ALL'); @@ -285,8 +284,6 @@ export const MyAssetsTable = ({ key={rowKey} className={`border-b border-gray-100 hover:bg-gray-50 transition-colors cursor-pointer ${index % 2 === 0 ? 'bg-transparent' : 'bg-gray-50/50' }`} - onMouseEnter={() => setHoveredRow(rowKey)} - onMouseLeave={() => setHoveredRow(null)} onClick={(e) => toggleRowExpansion(rowKey, e)} > {/* Expand Icon */} diff --git a/src/components/portfolio/NoAssetsModal.tsx b/src/components/portfolio/NoAssetsModal.tsx index d5662c6..7d37d12 100644 --- a/src/components/portfolio/NoAssetsModal.tsx +++ b/src/components/portfolio/NoAssetsModal.tsx @@ -1,6 +1,6 @@ // src/components/portfolio/NoAssetsModal.tsx import { useNavigate } from 'react-router-dom'; -import { Lock, Upload, ShoppingCart, Info, X } from 'lucide-react'; +import { Lock, ShoppingCart, Info, X } from 'lucide-react'; interface NoAssetsModalProps { isOpen: boolean; diff --git a/src/pages/admin/loans/LoansView.page.tsx b/src/pages/admin/loans/LoansView.page.tsx index 3c20536..505cedc 100644 --- a/src/pages/admin/loans/LoansView.page.tsx +++ b/src/pages/admin/loans/LoansView.page.tsx @@ -280,20 +280,6 @@ export function LoansView() { ); }; - // UI Helper: Health Status Badge - const getHealthStatusBadge = (healthStatus: string | undefined) => { - if (!healthStatus) return null; - - if (healthStatus === 'HEALTHY') { - return Healthy; - } else if (healthStatus === 'WARNING') { - return Warning; - } else if (healthStatus === 'CRITICAL' || healthStatus === 'LIQUIDATABLE') { - return Critical; - } - - return {healthStatus}; - }; // Calculate statistics const stats = { diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index bc30c30..fe63ab7 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -16,7 +16,7 @@ interface BorrowOnlyModalProps { creditData: OAIDCreditLine | null; } -export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: BorrowOnlyModalProps) => { +export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData}: BorrowOnlyModalProps) => { const [borrowAmount, setBorrowAmount] = useState(''); const [selectedPosition, setSelectedPosition] = useState(null); const [showPositionSelector, setShowPositionSelector] = useState(false); diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index df037bf..3cd8fa1 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -29,7 +29,7 @@ import { ShaderAnimation } from '../../components/ui/shimmer-lines'; import { useCreditData } from '../borrow/hooks/useCreditData'; import { DepositCollateralModal } from '../borrow/components/DepositCollateralModal'; import { NoAssetsModal } from '../../components/portfolio/NoAssetsModal'; -import { Wavy } from '../../components/ui/wavy'; +// import { Wavy } from '../../components/ui/wavy'; import HeroBackground from '../landing/HeroBackground'; From d659aa1aedc0fb590e877f7c4e3e2f715167f7dc Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 13:30:26 +0530 Subject: [PATCH 55/65] feat: Update Changelog and FAQ components with new features and UI enhancements --- .../changelog/ChangelogHotTimeline.tsx | 14 ++++ src/components/ui/faq-details.tsx | 32 +++++++- .../components/DepositCollateralModal.tsx | 30 ++++---- src/pages/landing/About.page.tsx | 74 +------------------ src/pages/landing/Changelog.page.tsx | 3 +- .../TradingEngine.page.tsx | 5 +- 6 files changed, 65 insertions(+), 93 deletions(-) diff --git a/src/components/changelog/ChangelogHotTimeline.tsx b/src/components/changelog/ChangelogHotTimeline.tsx index a4325e1..1388bbb 100644 --- a/src/components/changelog/ChangelogHotTimeline.tsx +++ b/src/components/changelog/ChangelogHotTimeline.tsx @@ -202,6 +202,20 @@ export function ChangelogTimeline() { height={500} className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]" /> + Server + Server
), diff --git a/src/components/ui/faq-details.tsx b/src/components/ui/faq-details.tsx index ad1dff6..4b0b4ff 100644 --- a/src/components/ui/faq-details.tsx +++ b/src/components/ui/faq-details.tsx @@ -136,18 +136,44 @@ const FaqDetails = ({ }, }, { - value: "borrow", + value: "credit line creation", + icon: , + label: "Credit Line", + content: { + badge: "Credit", + title: "Create credit line on your OAID", + description: + "Utilise RWA's as collateral to draw credit lines without moving assets.", + buttonText: "Create Credit Line", + videoSrc: "", + }, + }, + { + value: "borrowing against rwAs", icon: , label: "Borrow", content: { badge: "Credit", - title: "Access credit using OAID", + title: "Access credit using the credit line", description: - "Borrow against RWAs or private assets across native and partner protocols without moving collateral.", + "Borrow against RWAs or private assets across native and partner protocols", buttonText: "Borrow", videoSrc: "", }, }, + { + value: "repayment", + icon: , + label: "Repayment", + content: { + badge: "Credit", + title: "Easily repay loans and reclaim collateral", + description: + "Repay loans to restore credit and reclaim excess collateral", + buttonText: "Repay", + videoSrc: "", + }, + }, ], }: Feature108Props) => { const videoContainerRef = useRef(null); diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 937681d..9132a2b 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -313,12 +313,12 @@ export const DepositCollateralModal = ({ return (
-
+
{/* Close Button */} {!isProcessing && step === 'select' && ( @@ -326,7 +326,7 @@ export const DepositCollateralModal = ({ {/* Header */}
-
+
💰

@@ -354,7 +354,7 @@ export const DepositCollateralModal = ({ {/* Error State */} {error && !isLoading && (
- {error} + Something went wrong ! Please try again.
)} @@ -367,10 +367,10 @@ export const DepositCollateralModal = ({ -
+

Date TypeTx
{new Date(tx.date).toLocaleDateString('en-US', { month: 'short', diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 80a2376..67a9f26 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -177,7 +177,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr if (positions.length === 0) { return ( -
+
+
@@ -194,6 +195,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr Start Borrowing
+
); } @@ -249,7 +251,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr - + toggleRowExpansion(rowKey, e)} > @@ -316,7 +316,7 @@ export const TradesTable = ({ setOrderToCancel(order.orderId); }} disabled={isCancellingId === order.orderId} - className="flex items-center gap-1 px-3 py-1 bg-white border border-red-200 text-red-600 rounded-lg font-gellix text-xs font-medium hover:bg-red-50 hover:border-red-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" + className="flex items-center gap-1 px-3 py-1 bg-transparent border border-red-200 text-red-600 rounded-lg font-gellix text-xs font-medium hover:bg-red-50 hover:border-red-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" > {isCancellingId === order.orderId ? (
diff --git a/src/components/ui/wavy.tsx b/src/components/ui/wavy.tsx index 422e06c..0cdcf56 100644 --- a/src/components/ui/wavy.tsx +++ b/src/components/ui/wavy.tsx @@ -49,7 +49,7 @@ export function Wavy({ return ( -
+
{mounted && ( <> diff --git a/src/pages/faucet/Faucet.page.tsx b/src/pages/faucet/Faucet.page.tsx index 442aeaf..a808732 100644 --- a/src/pages/faucet/Faucet.page.tsx +++ b/src/pages/faucet/Faucet.page.tsx @@ -1,23 +1,23 @@ import { useAccount } from 'wagmi'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import { TokenFaucetCard } from '../../components/faucet/TokenFaucetCard'; +import { Wavy } from '../../components/ui/wavy'; const FaucetPage = () => { const { address } = useAccount(); return ( -
+
+ {/* Top Navigation Bar - Clean Marketplace Design */} -
- - + {/* Main Content */} -
+
{/* Page Header */}

Testnet Faucet

diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 0297205..a310596 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -29,6 +29,7 @@ import { ShaderAnimation } from '../../components/ui/shimmer-lines'; import { useCreditData } from '../borrow/hooks/useCreditData'; import { DepositCollateralModal } from '../borrow/components/DepositCollateralModal'; import { NoAssetsModal } from '../../components/portfolio/NoAssetsModal'; +import { Wavy } from '../../components/ui/wavy'; const PortfolioPage = () => { @@ -452,7 +453,7 @@ const PortfolioPage = () => { onClick={fetchPortfolio} variant="outline" size="lg" - className="bg-black/20 border-white/30 text-white hover:bg-white hover:text-black transition-all duration-300 backdrop-blur-md min-w-[200px]" + className="bg-black/20 border-white/30 text-white hover:bg-transparent hover:text-black transition-all duration-300 backdrop-blur-md min-w-[200px]" > Let's try again ! @@ -463,10 +464,17 @@ const PortfolioPage = () => { return ( <> - + {/* + */} + {/* + */} + + -
+ + +
{/* Top Navigation Bar - Fixed Height */}
@@ -498,20 +506,20 @@ const PortfolioPage = () => { {address && ( <> -
+
{truncateAddress(address)}
@@ -544,7 +552,7 @@ const PortfolioPage = () => { {/* Right Main Area - 3/4 width, tabbed content */}
{/* Single Table Container with Tabs */} -
{ `, }}> {/* Tab Header */} -
+
- - - - {/* Right: Wallet Display */} -
- {address && ( - <> - -
- {truncateAddress(address)} -
-
- -
- - )} + return (<> + +
+
+ + + {/* Page Header */} +
+
+
+

+ {currentAsset?.metadata?.invoiceNumber || 'Asset'} +

+ + {currentAsset?.metadata?.buyerName || 'Real World Asset'} +
-
-
- {isTradeable ? 'MARKET OPEN' : 'MARKET CLOSED'} +
+ {/* Center: Navigation */} + + + {/* Right: Wallet Display */} +
+ {address && ( + <> + +
+ {truncateAddress(address)} +
+
+ +
+ + )} +
+
+
+ {isTradeable ? 'MARKET OPEN' : 'MARKET CLOSED'} +
-
- {/* Main Content: 60/40 Split */} -
-
- {/* === LEFT COLUMN (60%): Orderbook + Charts (Scrollable) === */} -
- - {/* MARKET DEPTH (Orderbook) */} -
- {isLoadingOrderbook && !polling ? ( -
- -
- ) : ( - <> -
-

Market Depth

-
- - Buy Orders - - - Sell Orders - -
+ {/* Main Content: 60/40 Split */} +
+
+ {/* === LEFT COLUMN (60%): Orderbook + Charts (Scrollable) === */} +
+ + {/* MARKET DEPTH (Orderbook) */} +
+ {isLoadingOrderbook && !polling ? ( +
+
- -
- {/* BUY ORDERS (Bids) - Left Pane */} -
-
- Price - Amount - Total -
-
- {orderbook?.bids?.length > 0 ? orderbook.bids.map((level: PriceLevel, i: number) => { - const maxVolume = Math.max(...(orderbook.bids?.map((b: PriceLevel) => parseFloat(b.amountFormatted)) || [1])); - const width = (parseFloat(level.amountFormatted) / maxVolume) * 100; - return ( -
- {/* Depth Bar - positioned absolutely */} -
- - {level.orders.map((order) => { - const isMyOrder = address && order.maker.toLowerCase() === address.toLowerCase(); - return ( -
!isMyOrder && setSelectedOrder(order)} - title={isMyOrder ? "Creator cannot fulfill its own orders" : undefined} - className={`relative grid grid-cols-3 px-4 py-3 text-sm transition-colors ${isMyOrder - ? 'cursor-not-allowed opacity-60' - : 'cursor-pointer hover:bg-green-50/30' - }`} - > - ${parseFloat(level.priceFormatted).toFixed(2)} - {parseFloat(order.amountFormatted).toFixed(2)} - - ${(parseFloat(level.priceFormatted) * parseFloat(order.amountFormatted)).toFixed(2)} - -
- ); - })} -
- ); - }) : ( -
No active buy orders
- )} + ) : ( + <> +
+

Market Depth

+
+ + Buy Orders + + + Sell Orders +
- {/* SELL ORDERS (Asks) - Right Pane */} -
-
- Price - Amount - Total +
+ {/* BUY ORDERS (Bids) - Left Pane */} +
+
+ Price + Amount + Total +
+
+ {orderbook?.bids?.length > 0 ? orderbook.bids.map((level: PriceLevel, i: number) => { + const maxVolume = Math.max(...(orderbook.bids?.map((b: PriceLevel) => parseFloat(b.amountFormatted)) || [1])); + const width = (parseFloat(level.amountFormatted) / maxVolume) * 100; + return ( +
+ {/* Depth Bar - positioned absolutely */} +
+ + {level.orders.map((order) => { + const isMyOrder = address && order.maker.toLowerCase() === address.toLowerCase(); + return ( +
!isMyOrder && setSelectedOrder(order)} + title={isMyOrder ? "Creator cannot fulfill its own orders" : undefined} + className={`relative grid grid-cols-3 px-4 py-3 text-sm transition-colors ${isMyOrder + ? 'cursor-not-allowed opacity-60' + : 'cursor-pointer hover:bg-green-50/30' + }`} + > + ${parseFloat(level.priceFormatted).toFixed(2)} + {parseFloat(order.amountFormatted).toFixed(2)} + + ${(parseFloat(level.priceFormatted) * parseFloat(order.amountFormatted)).toFixed(2)} + +
+ ); + })} +
+ ); + }) : ( +
No active buy orders
+ )} +
-
- {orderbook?.asks?.length > 0 ? orderbook.asks.map((level: PriceLevel, i: number) => { - const maxVolume = Math.max(...(orderbook.asks?.map((a: PriceLevel) => parseFloat(a.amountFormatted)) || [1])); - const width = (parseFloat(level.amountFormatted) / maxVolume) * 100; - return ( -
- {/* Depth Bar */} -
- {level.orders.map((order) => { - const isMyOrder = address && order.maker.toLowerCase() === address.toLowerCase(); - return ( -
!isMyOrder && setSelectedOrder(order)} - title={isMyOrder ? "Creator cannot fulfill its own orders" : undefined} - className={`relative grid grid-cols-3 px-4 py-3 text-sm transition-colors ${isMyOrder - ? 'cursor-not-allowed opacity-60' - : 'cursor-pointer hover:bg-red-50/30' - }`} - > - ${parseFloat(level.priceFormatted).toFixed(2)} - {parseFloat(order.amountFormatted).toFixed(2)} - - ${(parseFloat(level.priceFormatted) * parseFloat(order.amountFormatted)).toFixed(2)} - -
- ); - })} -
- ); - }) : ( -
No active sell orders
- )} + + {/* SELL ORDERS (Asks) - Right Pane */} +
+
+ Price + Amount + Total +
+
+ {orderbook?.asks?.length > 0 ? orderbook.asks.map((level: PriceLevel, i: number) => { + const maxVolume = Math.max(...(orderbook.asks?.map((a: PriceLevel) => parseFloat(a.amountFormatted)) || [1])); + const width = (parseFloat(level.amountFormatted) / maxVolume) * 100; + return ( +
+ {/* Depth Bar */} +
+ {level.orders.map((order) => { + const isMyOrder = address && order.maker.toLowerCase() === address.toLowerCase(); + return ( +
!isMyOrder && setSelectedOrder(order)} + title={isMyOrder ? "Creator cannot fulfill its own orders" : undefined} + className={`relative grid grid-cols-3 px-4 py-3 text-sm transition-colors ${isMyOrder + ? 'cursor-not-allowed opacity-60' + : 'cursor-pointer hover:bg-red-50/30' + }`} + > + ${parseFloat(level.priceFormatted).toFixed(2)} + {parseFloat(order.amountFormatted).toFixed(2)} + + ${(parseFloat(level.priceFormatted) * parseFloat(order.amountFormatted)).toFixed(2)} + +
+ ); + })} +
+ ); + }) : ( +
No active sell orders
+ )} +
-
- - )} -
+ + )} +
- {/* PURCHASE ACTIVITY CHART */} -
-
-
-

Purchase Activity

-

Token purchases over time

-
-
-
- -
-

Total Activity

-

- {purchaseHistory?.totalTransactions || 0} -

-
+ {/* PURCHASE ACTIVITY CHART */} +
+
+
+

Purchase Activity

+

Token purchases over time

-
- -
-

Direct Buys

-

- {purchaseHistory?.metadata?.directPurchases || 0} -

+
+
+ +
+

Total Activity

+

+ {purchaseHistory?.totalTransactions || 0} +

+
-
-
- -
-

Leveraged

-

- {purchaseHistory?.metadata?.leveragePurchases || 0} -

+
+ +
+

Direct Buys

+

+ {purchaseHistory?.metadata?.directPurchases || 0} +

+
+
+
+ +
+

Leveraged

+

+ {purchaseHistory?.metadata?.leveragePurchases || 0} +

+
-
-
- {formattedChartData.length > 0 ? ( - - - - - - - - - { - const date = new Date(timestamp); - return date.toLocaleString(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }} - /> - { - if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; - if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; - return tokens.toFixed(0); - }} - label={{ value: 'Tokens Purchased', angle: -90, position: 'insideRight', style: { fill: '#6B7280', fontSize: 12 } }} - /> - { - const tokens = typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : value; - return [ -
-

{tokens} Tokens

- {props.payload.purchaseMethod && ( -

- Method: - {props.payload.purchaseMethod} - -

- )} -

{props.payload.purchaseCount} transaction(s)

-
, - '' - ]; - }} - labelFormatter={(timestamp) => { - const date = new Date(timestamp); - return date.toLocaleString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }} - /> - -
-
- ) : ( -
- -

No purchase activity yet

-
- )} +
+ {formattedChartData.length > 0 ? ( + + + + + + + + + { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }} + /> + { + if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; + if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; + return tokens.toFixed(0); + }} + label={{ value: 'Tokens Purchased', angle: -90, position: 'insideRight', style: { fill: '#6B7280', fontSize: 12 } }} + /> + { + const tokens = typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : value; + return [ +
+

{tokens} Tokens

+ {props.payload.purchaseMethod && ( +

+ Method: + {props.payload.purchaseMethod} + +

+ )} +

{props.payload.purchaseCount} transaction(s)

+
, + '' + ]; + }} + labelFormatter={(timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }} + /> + +
+
+ ) : ( +
+ +

No purchase activity yet

+
+ )} +
- {/* P2P TRADING CHARTS */} - - -
- - {/* === RIGHT COLUMN (40%): Buy/Sell Panel + Asset Details (Sticky) === */} -
- {/* EXECUTION ZONE (Buy/Sell Panel) */} -
- {/* CONTEXTUAL PANEL: Order Review State */} - {selectedOrder ? ( -
-
-

Order Review

- -
+ {/* === RIGHT COLUMN (40%): Buy/Sell Panel + Asset Details (Sticky) === */} +
+ {/* EXECUTION ZONE (Buy/Sell Panel) */} +
+ {/* CONTEXTUAL PANEL: Order Review State */} + {selectedOrder ? ( +
+
+

Order Review

+ +
-
- {/* Settlement Summary */} -
-
- Price per Token - ${selectedOrder.priceFormatted} -
-
- Amount - - {selectedOrder.amountFormatted} Tokens - -
-
-
- Total Settlement - - ${(parseFloat(selectedOrder.amountFormatted) * parseFloat(selectedOrder.priceFormatted)).toFixed(2)} - +
+ {/* Settlement Summary */} +
+
+ Price per Token + ${selectedOrder.priceFormatted} +
+
+ Amount + + {selectedOrder.amountFormatted} Tokens + +
+
+
+ Total Settlement + + ${(parseFloat(selectedOrder.amountFormatted) * parseFloat(selectedOrder.priceFormatted)).toFixed(2)} + +
-
- {/* Transaction Progress */} - {/* {(isTxPending || isTxConfirming) && ( + {/* Transaction Progress */} + {/* {(isTxPending || isTxConfirming) && (
@@ -847,240 +846,250 @@ const TradingEngineProductionPage = () => {
)} */} - {/* Execute Button */} - - -
- Secured by Mantle Network -
-
-
- ) : ( - /* CONTEXTUAL PANEL: Place Order State */ -
-
-

Place Order

-
+ {/* Execute Button */} - + +
+ Secured by Mantle Network +
- - {/* Input Fields */} -
-
- -
- setAmount(e.target.value)} - placeholder="0.00" - className="w-full h-16 pl-5 pr-20 bg-neutral-100/10 border border-gray-200 rounded-2xl font-gellix text-xl font-bold text-[#111111] focus:ring-2 focus:ring-[#0071C5] focus:border-transparent outline-none transition-all placeholder:text-[#D1D5DB]" - disabled={!isTradeable} - /> -
- TOKENS -
+ ) : ( + /* CONTEXTUAL PANEL: Place Order State */ +
+
+

Place Order

+
+ +
-
- -
- setPrice(e.target.value)} - placeholder="0.00" - className="w-full h-16 pl-5 pr-20 bg-neutral-100/10 border border-gray-200 rounded-2xl font-gellix text-xl font-bold text-[#111111] focus:ring-2 focus:ring-[#0071C5] focus:border-transparent outline-none transition-all placeholder:text-[#D1D5DB]" - disabled={!isTradeable} - /> -
- USDC + {/* Input Fields */} +
+
+ +
+ setAmount(e.target.value)} + placeholder="0.00" + className="w-full h-16 pl-5 pr-20 bg-neutral-100/10 border border-gray-200 rounded-2xl font-gellix text-xl font-bold text-[#111111] focus:ring-2 focus:ring-[#0071C5] focus:border-transparent outline-none transition-all placeholder:text-[#D1D5DB]" + disabled={!isTradeable} + /> +
+ TOKENS +
-
-
- {/* Balances & Info */} -
-
- Available {orderType === 'buy' ? 'USDC' : 'Tokens'} - - {orderType === 'buy' - ? `$${usdcBalance ? parseFloat(formatUnits(usdcBalance, 6)).toFixed(2) : '0.00'}` - : `${tokenBalance ? parseFloat(formatUnits(tokenBalance, 18)).toFixed(2) : '0.00'}` - } - +
+ +
+ setPrice(e.target.value)} + placeholder="0.00" + className="w-full h-16 pl-5 pr-20 bg-neutral-100/10 border border-gray-200 rounded-2xl font-gellix text-xl font-bold text-[#111111] focus:ring-2 focus:ring-[#0071C5] focus:border-transparent outline-none transition-all placeholder:text-[#D1D5DB]" + disabled={!isTradeable} + /> +
+ USDC +
+
+
- {orderType === 'sell' && needsTokenApproval && ( -
- - One-time approval required to trade this asset -
- )} - {orderType === 'buy' && needsUsdcApproval && ( -
- - One-time approval required to spend USDC + {/* Balances & Info */} +
+
+ Available {orderType === 'buy' ? 'USDC' : 'Tokens'} + + {orderType === 'buy' + ? `$${usdcBalance ? parseFloat(formatUnits(usdcBalance, 6)).toFixed(2) : '0.00'}` + : `${tokenBalance ? parseFloat(formatUnits(tokenBalance, 18)).toFixed(2) : '0.00'}` + } +
- )} -
- {/* Order Summary */} - {amount && price && ( -
-
- Estimated Total -
- - ${(parseFloat(amount) * parseFloat(price)).toFixed(2)} - - USDC + {orderType === 'sell' && needsTokenApproval && ( +
+ + One-time approval required to trade this asset
-
+ )} + {orderType === 'buy' && needsUsdcApproval && ( +
+ + One-time approval required to spend USDC +
+ )}
- )} - {/* Create Order Button */} - - - {(isTxPending || isTxConfirming) && txHash && ( - +
+ Estimated Total +
+ + ${(parseFloat(amount) * parseFloat(price)).toFixed(2)} + + USDC +
+
+
+ )} + + {/* Create Order Button */} + -
- Secured by Mantle Network -
-
- )} -
+ {(isTxPending || isTxConfirming) && txHash && ( + + View on Explorer → + + )} - {/* ASSET DETAILS */} -
- {isLoadingAsset ? ( -
- -
- ) : ( - <> -

Asset Details

-
-
-

Face Value

-

- ${currentAsset?.metadata?.faceValue - ? (parseFloat(currentAsset.metadata.faceValue)).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) - : '0.00'} -

-
-
-

Issue Date

-

- {currentAsset?.metadata?.issueDate - ? new Date(currentAsset.metadata.issueDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) - : 'N/A'} -

-
-
-

Due Date

-

- {currentAsset?.metadata?.dueDate - ? new Date(currentAsset.metadata.dueDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) - : 'N/A'} -

-
-
-

Total Supply

-

- {currentAsset?.tokenParams?.totalSupply - ? (parseFloat(currentAsset.tokenParams.totalSupply) / 1e18).toLocaleString() - : '0'} tokens -

-
-
-

Buyer

-

{currentAsset?.metadata?.buyerName || 'N/A'}

-
-
-

Industry

-

{currentAsset?.metadata?.industry || 'N/A'}

-
-
-

Risk Tier

-

{currentAsset?.metadata?.riskTier || 'N/A'}

-
-
-

Token Address

-

- {currentAsset?.token?.address || 'N/A'} -

+
+ Secured by Mantle Network
- - )} + )} +
+ + {/* ASSET DETAILS */} +
+ {isLoadingAsset ? ( +
+ +
+ ) : ( + <> +

Asset Details

+
+
+

Face Value

+

+ ${currentAsset?.metadata?.faceValue + ? (parseFloat(currentAsset.metadata.faceValue)).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + : '0.00'} +

+
+
+

Issue Date

+

+ {currentAsset?.metadata?.issueDate + ? new Date(currentAsset.metadata.issueDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + : 'N/A'} +

+
+
+

Due Date

+

+ {currentAsset?.metadata?.dueDate + ? new Date(currentAsset.metadata.dueDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + : 'N/A'} +

+
+
+

Total Supply

+

+ {currentAsset?.tokenParams?.totalSupply + ? (parseFloat(currentAsset.tokenParams.totalSupply) / 1e18).toLocaleString() + : '0'} tokens +

+
+
+

Buyer

+

{currentAsset?.metadata?.buyerName || 'N/A'}

+
+
+

Industry

+

{currentAsset?.metadata?.industry || 'N/A'}

+
+
+

Risk Tier

+

{currentAsset?.metadata?.riskTier || 'N/A'}

+
+
+

Token Address

+

+ {currentAsset?.token?.address || 'N/A'} +

+
+
+ + )} +
+
+ Drag chart or use slider to explore historical trends +
+
+ + +
+ + ); }; From 569d17fe9562cdc82f79065bea8155d5540d6548 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sun, 11 Jan 2026 00:06:00 +0530 Subject: [PATCH 12/65] fix: correct polling intervals in TradingEngine.page.tsx for accurate data fetching --- src/components/leverage/PositionsTable.tsx | 15 ++++- src/components/portfolio/ActiveBidsTable.tsx | 2 +- src/pages/portfolio/Portfolio.page.tsx | 66 +++++++++---------- .../TradingEngine.page.tsx | 4 +- 4 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/components/leverage/PositionsTable.tsx b/src/components/leverage/PositionsTable.tsx index baba7c8..75e8b59 100644 --- a/src/components/leverage/PositionsTable.tsx +++ b/src/components/leverage/PositionsTable.tsx @@ -173,7 +173,7 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit <>
{/* Filter Bar */} -
+
Filter by Status: @@ -200,7 +200,7 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit
@@ -215,7 +215,7 @@ export const TradesTable = ({ <>
- + + @@ -227,6 +230,7 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit const health = getHealthFactor(pos); const collateral = formatMETH(pos.mETHCollateral); const invested = formatUSDC(pos.usdcBorrowed); + const totalAmount = isPortfolioPosition(pos) ? formatMETH(pos.totalAmount) : (parseFloat(formatMETH((pos as LeveragePosition).rwaTokenAmount || '0' ))).toFixed(2); const status = getStatus(pos); const positionStatusStyle = getPositionStatusStyle(status); const isActivePosition = status === 'ACTIVE'; @@ -259,6 +263,13 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit + {/* Tokens */} + + {/* Collateral */}
Asset @@ -208,6 +208,9 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit Amount Invested + Tokens + Collateral +
+ {totalAmount} +
+
diff --git a/src/components/portfolio/ActiveBidsTable.tsx b/src/components/portfolio/ActiveBidsTable.tsx index a2b5d18..470ad52 100644 --- a/src/components/portfolio/ActiveBidsTable.tsx +++ b/src/components/portfolio/ActiveBidsTable.tsx @@ -80,7 +80,7 @@ export const ActiveBidsTable = ({
{/* Filter Bar */}
-
+
Filter by Status:
diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index a310596..ebe3c5f 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -65,23 +65,21 @@ const PortfolioPage = () => { // Solvency loans state const [myLoans, setMyLoans] = useState([]); const [isLoadingMyLoans, setIsLoadingMyLoans] = useState(true); + const { settleBid, status: settleStatus, error: settleError, isLoading: isSettling, isSuccess } = useSettleBid(); + const [settlingBidId, setSettlingBidId] = useState(null); - // Fetch solvency loans - const fetchMyLoans = useCallback(async () => { - if (!address) return; - setIsLoadingMyLoans(true); - try { - const response = await solvencyService.getMyPositions('ACTIVE', 100, 0); - const loans = response.positions.filter(p => parseFloat(p.usdcBorrowed) > 0); - setMyLoans(loans); - } catch (err) { - console.error("Error fetching solvency loans:", err); - showError("Failed to fetch loans", "Could not retrieve your loan positions."); - } finally { - setIsLoadingMyLoans(false); - } - }, [address, showError]); - + // Yield claiming state (investor-claim-yield.sh burn-to-claim model) + const [claimingAssetId, setClaimingAssetId] = useState(null); + const [claimStatus, setClaimStatus] = useState(''); + const [showClaimModal, setShowClaimModal] = useState(false); + const [selectedAssetForClaim, setSelectedAssetForClaim] = useState<{ + assetId: string; + tokenAddress: string; + tokenSymbol: string; + investorBalance: string; + expectedUsdc: string; + allowance: string; + } | null>(null); // Filtered data based on search term // Get all portfolio items (both STATIC and LEVERAGE) @@ -120,23 +118,22 @@ const PortfolioPage = () => { 0 ); - // Contract interaction for settling bids (investor-settle.sh verified) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { settleBid, status: settleStatus, error: settleError, isLoading: isSettling, isSuccess } = useSettleBid(); - const [settlingBidId, setSettlingBidId] = useState(null); - // Yield claiming state (investor-claim-yield.sh burn-to-claim model) - const [claimingAssetId, setClaimingAssetId] = useState(null); - const [claimStatus, setClaimStatus] = useState(''); - const [showClaimModal, setShowClaimModal] = useState(false); - const [selectedAssetForClaim, setSelectedAssetForClaim] = useState<{ - assetId: string; - tokenAddress: string; - tokenSymbol: string; - investorBalance: string; - expectedUsdc: string; - allowance: string; - } | null>(null); + // Fetch solvency loans + const fetchMyLoans = useCallback(async () => { + if (!address) return; + setIsLoadingMyLoans(true); + try { + const response = await solvencyService.getMyPositions('ACTIVE', 100, 0); + const loans = response.positions.filter(p => parseFloat(p.usdcBorrowed) > 0); + setMyLoans(loans); + } catch (err) { + console.error("Error fetching solvency loans:", err); + showError("Failed to fetch loans", "Could not retrieve your loan positions."); + } finally { + setIsLoadingMyLoans(false); + } + }, [address, showError]); useEffect(() => { fetchPortfolio(); @@ -185,6 +182,7 @@ const PortfolioPage = () => { fetchMyOrders(); // Refresh orders } }, [isCancelSuccess]); + const handleIncreaseCreditLimit = () => { if (portfolio && portfolio.portfolio.length > 0) { setShowDepositModal(true); @@ -468,9 +466,7 @@ const PortfolioPage = () => { */} {/* */} - - - + diff --git a/src/pages/secondary-marketplace/TradingEngine.page.tsx b/src/pages/secondary-marketplace/TradingEngine.page.tsx index 2d0c4eb..41e058a 100644 --- a/src/pages/secondary-marketplace/TradingEngine.page.tsx +++ b/src/pages/secondary-marketplace/TradingEngine.page.tsx @@ -144,7 +144,7 @@ const TradingEngineProductionPage = () => { useEffect(() => { fetchChartData(); - const poll = setInterval(fetchChartData, 30000); + const poll = setInterval(fetchChartData, 30000000); echarts.connect('trading-engine'); return () => clearInterval(poll); }, [fetchChartData]); @@ -218,7 +218,7 @@ const TradingEngineProductionPage = () => { if (!assetId) return; fetchOrderbook(assetId); setPolling(true); - const poll = setInterval(() => { fetchOrderbook(assetId); setPolling(true); }, 2000); + const poll = setInterval(() => { fetchOrderbook(assetId); setPolling(true); }, 200000000); return () => { clearInterval(poll); setPolling(false); }; }, [fetchOrderbook]); From 631a81ad30f06bc274e15acd60210ab94d14fe3f Mon Sep 17 00:00:00 2001 From: Dead-Bytes Date: Sun, 11 Jan 2026 11:07:31 +0530 Subject: [PATCH 13/65] "added bid enums" --- src/types/marketplace.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/marketplace.types.ts b/src/types/marketplace.types.ts index c5f55fb..b7c4692 100644 --- a/src/types/marketplace.types.ts +++ b/src/types/marketplace.types.ts @@ -272,7 +272,7 @@ export type SortOption = // ============================================================================ export type AuctionStatus = 'SCHEDULED' | 'BIDDING' | 'ENDED' | 'SETTLED' | 'CANCELLED'; -export type BidStatus = 'PENDING' | 'WON' | 'LOST' | 'SETTLED' | 'REFUNDED'; +export type BidStatus = 'PENDING' | 'PLACED' | 'FINALIZED' | 'WON' | 'LOST' | 'SETTLED' | 'REFUNDED'; /** * Auction - Represents an RWA token auction From 8c40ab02f15826dadeb64cf8d610ab38160c4711 Mon Sep 17 00:00:00 2001 From: Dead-Bytes Date: Sun, 11 Jan 2026 11:07:40 +0530 Subject: [PATCH 14/65] "connected loans" --- docs/backend/LOAN_SYNC_IMPLEMENTATION.md | 266 ++++++++++++++++++ src/components/portfolio/ActiveBidsTable.tsx | 10 +- src/components/portfolio/RepayLoanModal.tsx | 15 +- src/lib/api/solvency-contract.service.ts | 46 ++- src/lib/api/solvency.service.ts | 104 ++++++- .../borrow/components/DirectBorrowModal.tsx | 17 +- .../borrow/components/UnifiedBorrowModal.tsx | 9 +- .../auction/AuctionDetails.page.tsx | 6 +- src/pages/portfolio/Portfolio.page.tsx | 2 +- src/types/solvency.types.ts | 22 +- 10 files changed, 453 insertions(+), 44 deletions(-) create mode 100644 docs/backend/LOAN_SYNC_IMPLEMENTATION.md diff --git a/docs/backend/LOAN_SYNC_IMPLEMENTATION.md b/docs/backend/LOAN_SYNC_IMPLEMENTATION.md new file mode 100644 index 0000000..6699b7a --- /dev/null +++ b/docs/backend/LOAN_SYNC_IMPLEMENTATION.md @@ -0,0 +1,266 @@ +# Loan Sync Implementation Summary + +**Date**: 2026-01-11 +**Issue**: Backend database not syncing with on-chain loan transactions + +--- + +## Problem + +When users borrowed or repaid loans directly via smart contracts (using scripts), the backend database was not updated. This caused: + +1. **Stale Data**: `usdcBorrowed` remained at "0" even after borrowing +2. **Missing Loan Details**: `loanDuration`, `numberOfInstallments`, `repaymentSchedule` were not populated +3. **No Repayment Tracking**: Repayments didn't update `totalRepaid` or mark installments as paid + +**Example**: Position showing `usdcBorrowed: "0"` even though user borrowed and repaid on-chain. + +--- + +## Solution + +Created **loan notification endpoints** that frontend/scripts call after on-chain transactions to sync the backend database. + +--- + +## Implementation + +### 1. New DTOs + +**`NotifyLoanBorrowDto`** ([notify-loan-borrow.dto.ts](packages/backend/src/modules/solvency/dto/notify-loan-borrow.dto.ts)): +```typescript +{ + txHash: string; // Transaction hash + positionId: string; // Position ID + borrowAmount: string; // USDC borrowed (6 decimals) + loanDuration: string; // Duration in seconds + numberOfInstallments: string; + blockNumber?: string; +} +``` + +**`NotifyLoanRepaymentDto`** ([notify-loan-repayment.dto.ts](packages/backend/src/modules/solvency/dto/notify-loan-repayment.dto.ts)): +```typescript +{ + txHash: string; + positionId: string; + repaymentAmount: string; // USDC repaid (6 decimals) + blockNumber?: string; +} +``` + +### 2. Service Methods + +**[solvency-position.service.ts](packages/backend/src/modules/solvency/services/solvency-position.service.ts:552-625)** + +**`notifyLoanBorrow()`**: +- Verifies position belongs to user +- Calls `recordBorrow()` to update database +- Updates: `usdcBorrowed`, `loanDuration`, `numberOfInstallments`, `repaymentSchedule` +- Calculates and stores repayment schedule + +**`notifyLoanRepayment()`**: +- Verifies position belongs to user +- Calls `recordRepayment()` to update database +- Updates: `usdcBorrowed`, `totalRepaid`, `installmentsPaid`, schedule status +- Marks position as REPAID if fully paid + +### 3. Controller Endpoints + +**[solvency.controller.ts](packages/backend/src/modules/solvency/controllers/solvency.controller.ts:527-559)** + +**`POST /solvency/loan/borrow-notify`** +```typescript +@Post('loan/borrow-notify') +@HttpCode(HttpStatus.OK) +@UseGuards(JwtAuthGuard) +async notifyLoanBorrow(@Request() req: any, @Body() dto: NotifyLoanBorrowDto) +``` + +**`POST /solvency/loan/repay-notify`** +```typescript +@Post('loan/repay-notify') +@HttpCode(HttpStatus.OK) +@UseGuards(JwtAuthGuard) +async notifyLoanRepayment(@Request() req: any, @Body() dto: NotifyLoanRepaymentDto) +``` + +### 4. Updated Scripts + +**[borrow-solvency-loan.js](scripts/borrow-solvency-loan.js:262-296)** + +Added `notifyBackendOfBorrow()` function: +- Called automatically after successful borrow transaction +- Sends loan details to backend +- Syncs database with on-chain state + +**[repay-solvency-loan.js](scripts/repay-solvency-loan.js:237-270)** + +Updated `syncRepaymentWithBackend()` function: +- Calls `/solvency/loan/repay-notify` endpoint +- Syncs repayment with backend database + +--- + +## Usage + +### Borrow Flow + +1. **User borrows via contract**: +```bash +INVESTOR_KEY=0x... node scripts/borrow-solvency-loan.js 1 50 6 +``` + +2. **Script automatically**: + - Executes `borrowUSDC()` on-chain + - Calls `POST /solvency/loan/borrow-notify` + - Syncs loan details to database + +3. **Backend updates**: + - `usdcBorrowed`: "50000000" (50 USDC) + - `loanDuration`: calculated from asset maturity + - `numberOfInstallments`: 6 + - `repaymentSchedule`: array of 6 installments with due dates + +### Repay Flow + +1. **User repays via contract**: +```bash +INVESTOR_KEY=0x... node scripts/repay-solvency-loan.js 1 10 +``` + +2. **Script automatically**: + - Executes `repayLoan()` on-chain + - Calls `POST /solvency/loan/repay-notify` + - Syncs repayment to database + +3. **Backend updates**: + - `usdcBorrowed`: reduced by principal amount + - `totalRepaid`: increased by repayment amount + - `installmentsPaid`: incremented + - `repaymentSchedule[0].status`: marked as "PAID" + - `status`: "REPAID" if fully paid + +### Frontend Integration + +```typescript +// After user borrows +const response = await fetch('/solvency/loan/borrow-notify', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${userToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + txHash: borrowTxHash, + positionId: "1", + borrowAmount: "50000000", + loanDuration: "2592000", + numberOfInstallments: "6", + blockNumber: receipt.blockNumber.toString() + }) +}); + +// After user repays +const response = await fetch('/solvency/loan/repay-notify', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${userToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + txHash: repayTxHash, + positionId: "1", + repaymentAmount: "10000000", + blockNumber: receipt.blockNumber.toString() + }) +}); +``` + +--- + +## Security + +- **Authentication**: Both endpoints require `JwtAuthGuard` +- **Authorization**: Verifies position belongs to authenticated user +- **Validation**: DTOs validate all input parameters +- **Optional TX Verification**: Comments indicate where on-chain verification can be added + +--- + +## Benefits + +1. **Real-time Sync**: Database immediately updated after transactions +2. **Accurate Data**: Frontend/backend show same state as blockchain +3. **Repayment Tracking**: Full schedule with installment status +4. **No Event Indexer Required**: Works without blockchain event listeners +5. **User-Friendly**: Automatic sync in scripts, easy frontend integration + +--- + +## Testing + +### Test Borrow Notification + +```bash +# 1. Borrow loan (automatically syncs) +INVESTOR_KEY=0x... node scripts/borrow-solvency-loan.js 1 50 6 + +# 2. Check database - should show: +# - usdcBorrowed: "50000000" +# - loanDuration: set +# - numberOfInstallments: 6 +# - repaymentSchedule: 6 installments +``` + +### Test Repayment Notification + +```bash +# 1. Repay loan (automatically syncs) +INVESTOR_KEY=0x... node scripts/repay-solvency-loan.js 1 10 + +# 2. Check database - should show: +# - usdcBorrowed: reduced +# - totalRepaid: increased +# - installmentsPaid: incremented +# - repaymentSchedule[0].status: "PAID" +``` + +--- + +## Related Fixes + +This implementation follows the same pattern as: +- `/marketplace/purchases/notify` - Purchase notifications +- `/marketplace/bids/notify` - Bid notifications +- `/marketplace/bids/settle-notify` - Settlement notifications +- `/solvency/sync-position` - Position sync (fixed to upsert instead of always creating) + +--- + +## Files Changed + +### Backend +- ✅ `dto/notify-loan-borrow.dto.ts` - New DTO +- ✅ `dto/notify-loan-repayment.dto.ts` - New DTO +- ✅ `services/solvency-position.service.ts` - Added notify methods +- ✅ `controllers/solvency.controller.ts` - Added endpoints + +### Scripts +- ✅ `borrow-solvency-loan.js` - Added backend sync +- ✅ `repay-solvency-loan.js` - Added backend sync + +--- + +## Future Enhancements + +1. **On-Chain Verification**: Add transaction verification in notify methods +2. **Event Indexer**: Optional blockchain event listener as backup +3. **Webhooks**: Notify external systems of loan events +4. **Batch Sync**: Endpoint to sync multiple positions at once + +--- + +**Status**: ✅ Complete and working +**Build**: ✅ Successful +**Ready for**: Testing and deployment diff --git a/src/components/portfolio/ActiveBidsTable.tsx b/src/components/portfolio/ActiveBidsTable.tsx index ef7c49c..acf9957 100644 --- a/src/components/portfolio/ActiveBidsTable.tsx +++ b/src/components/portfolio/ActiveBidsTable.tsx @@ -30,16 +30,20 @@ export const ActiveBidsTable = ({ ? bids : bids.filter(bid => bid.status === statusFilter); - const statusOptions: Array = ['ALL', 'PENDING', 'WON', 'LOST', 'SETTLED', 'REFUNDED']; + const statusOptions: Array = ['ALL', 'PENDING', 'PLACED', 'FINALIZED', 'WON', 'LOST', 'SETTLED', 'REFUNDED']; const getBidStatusStyle = (status: BidStatus) => { switch (status) { + case 'PLACED': + return { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Placed' }; + case 'FINALIZED': + return { bg: 'bg-purple-100', text: 'text-purple-700', label: 'Finalized' }; case 'WON': - return { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Won' }; + return { bg: 'bg-green-100', text: 'text-green-700', label: 'Won' }; case 'LOST': return { bg: 'bg-red-100', text: 'text-red-700', label: 'Lost' }; case 'SETTLED': - return { bg: 'bg-green-100', text: 'text-green-700', label: 'Settled' }; + return { bg: 'bg-emerald-100', text: 'text-emerald-700', label: 'Settled' }; case 'REFUNDED': return { bg: 'bg-gray-100', text: 'text-gray-700', label: 'Refunded' }; case 'PENDING': diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 988121d..2f7543b 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -143,20 +143,21 @@ export const RepayLoanModal = ({ setCurrentStep('syncing'); - // Step 3: Sync position with backend + // Step 3: Notify backend of loan repayment setCurrentStep('syncing'); - console.log('🔄 Syncing position with backend...'); + console.log('🔄 Notifying backend of loan repayment...'); try { - await solvencyService.syncPosition({ - positionId: position.positionId.toString(), + await solvencyService.notifyLoanRepayment({ txHash: repayResult.txHash!, - blockNumber: repayResult.blockNumber!, + positionId: position.positionId.toString(), + repaymentAmount: amountWei.toString(), + blockNumber: repayResult.blockNumber?.toString(), }); - console.log('✅ Position synced with backend'); + console.log('✅ Backend notified of loan repayment'); } catch (syncError) { // Non-blocking: Events will still sync it automatically - console.warn('⚠️ Manual sync failed (events will auto-sync):', syncError); + console.warn('⚠️ Manual notification failed (events will auto-sync):', syncError); } // Success diff --git a/src/lib/api/solvency-contract.service.ts b/src/lib/api/solvency-contract.service.ts index f888096..9358b4e 100644 --- a/src/lib/api/solvency-contract.service.ts +++ b/src/lib/api/solvency-contract.service.ts @@ -81,12 +81,18 @@ class SolvencyContractService { * Get provider and signer from browser wallet */ private async getProviderAndSigner() { + console.log('🔌 Getting provider and signer...'); + if (!window.ethereum) { throw new Error('No wallet found. Please install MetaMask or another Web3 wallet.'); } + console.log(' Creating BrowserProvider...'); const provider = new ethers.BrowserProvider(window.ethereum); + + console.log(' Getting network...'); const network = await provider.getNetwork(); + console.log(` Connected to network: chainId ${network.chainId}`); // Check if on Mantle Sepolia (chainId: 5003) if (network.chainId !== 5003n) { @@ -95,7 +101,10 @@ class SolvencyContractService { ); } + console.log(' Getting signer...'); const signer = await provider.getSigner(); + console.log(` Signer address: ${await signer.getAddress()}`); + return { provider, signer }; } @@ -103,8 +112,14 @@ class SolvencyContractService { * Get vault contract instance */ private async getVaultContract() { + console.log('📄 Getting vault contract instance...'); + console.log(` Vault address: ${VAULT_CONTRACT_ADDRESS}`); + const { signer } = await this.getProviderAndSigner(); - return new ethers.Contract(VAULT_CONTRACT_ADDRESS, SOLVENCY_VAULT_ABI, signer); + const contract = new ethers.Contract(VAULT_CONTRACT_ADDRESS, SOLVENCY_VAULT_ABI, signer); + + console.log('✅ Vault contract created successfully'); + return contract; } /** @@ -418,21 +433,34 @@ class SolvencyContractService { numberOfInstallments, }); + console.log('🔍 Step 1: Getting vault contract...'); const vault = await this.getVaultContract(); + console.log('✅ Vault contract obtained:', vault.target); + console.log(`📝 Step 2: Preparing transaction...`); console.log(` Borrowing $${ethers.formatUnits(amount, 6)} USDC from SeniorPool...`); console.log(` Loan Duration: ${loanDuration} seconds (~${Math.floor(loanDuration / 86400)} days)`); console.log(` Installments: ${numberOfInstallments}`); + console.log('🚀 Step 3: Calling vault.borrowUSDC()...'); + console.log(' Parameters:', { + positionId, + amount: amount.toString(), + loanDuration, + numberOfInstallments + }); + const tx = await vault.borrowUSDC(positionId, amount, loanDuration, numberOfInstallments); - console.log(` Transaction submitted: ${tx.hash}`); - console.log('⏳ Waiting for confirmation...'); + console.log(`✅ Step 4: Transaction submitted successfully!`); + console.log(` Transaction hash: ${tx.hash}`); + console.log('⏳ Step 5: Waiting for confirmation...'); const receipt = await tx.wait(); - console.log(`✅ Borrow confirmed in block ${receipt.blockNumber}`); + console.log(`✅ Step 6: Transaction confirmed in block ${receipt.blockNumber}`); // Parse USDCBorrowed event + console.log('🔍 Step 7: Parsing events...'); let borrowed = null; let totalDebt = null; for (const log of receipt.logs) { @@ -455,7 +483,8 @@ class SolvencyContractService { } } - console.log(` Explorer: https://explorer.sepolia.mantle.xyz/tx/${tx.hash}`); + console.log(`🔗 Explorer: https://explorer.sepolia.mantle.xyz/tx/${tx.hash}`); + console.log('✅ Step 8: Borrow completed successfully!'); return { success: true, @@ -463,7 +492,12 @@ class SolvencyContractService { blockNumber: receipt.blockNumber, }; } catch (error: any) { - console.error('❌ Borrow failed:', error); + console.error('❌ Borrow failed at some step:', error); + console.error(' Error name:', error.name); + console.error(' Error message:', error.message); + console.error(' Error code:', error.code); + if (error.reason) console.error(' Error reason:', error.reason); + if (error.data) console.error(' Error data:', error.data); return { success: false, error: error.message || 'Borrow transaction failed', diff --git a/src/lib/api/solvency.service.ts b/src/lib/api/solvency.service.ts index 79ad1ab..b0daa8d 100644 --- a/src/lib/api/solvency.service.ts +++ b/src/lib/api/solvency.service.ts @@ -106,7 +106,7 @@ class SolvencyService extends BaseService { /** * Get user's borrow positions * - * ✅ CORRECT ENDPOINT: GET /solvency/positions/my?status=ACTIVE&limit=20&offset=0 + * ✅ CORRECT ENDPOINT: GET /solvency/positions/my * Reference: SOLVENCY_INTEGRATION.md line 157 * * BACKEND RESPONSE (line 160-188 in docs): @@ -115,18 +115,12 @@ class SolvencyService extends BaseService { * meta: { total, limit, offset } * } */ - async getMyPositions(status: string = 'ACTIVE', limit: number = 20, offset: number = 0): Promise { + async getMyPositions(): Promise { try { console.log('📊 Fetching user positions...'); - const params = new URLSearchParams({ - status, - limit: limit.toString(), - offset: offset.toString(), - }); - const response = await this.fetchWithTimeout( - `${this.baseURL}/solvency/positions/my?${params}`, + `${this.baseURL}/solvency/positions/my`, { method: 'GET', headers: this.getAuthHeaders(), @@ -715,6 +709,98 @@ class SolvencyService extends BaseService { } } + /** + * Notify backend of loan borrow transaction + * + * ✅ ENDPOINT: POST /solvency/loan/borrow-notify + * Reference: LOAN_SYNC_IMPLEMENTATION.md + * + * Call after successful borrow transaction to sync backend database + */ + async notifyLoanBorrow(request: { + txHash: string; + positionId: string; + borrowAmount: string; + loanDuration: string; + numberOfInstallments: string; + blockNumber?: string; + }): Promise<{ + success: boolean; + message: string; + position: any; + }> { + try { + console.log('📢 Notifying backend of loan borrow:', request); + + const response = await this.fetchWithTimeout( + `${this.baseURL}/solvency/loan/borrow-notify`, + { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify(request), + }, + 60000 // 60 second timeout + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to notify loan borrow'); + } + + const data = await response.json(); + console.log('✅ Loan borrow notification successful:', data); + return data; + } catch (error: any) { + console.error('❌ Error notifying loan borrow:', error); + throw error; + } + } + + /** + * Notify backend of loan repayment transaction + * + * ✅ ENDPOINT: POST /solvency/loan/repay-notify + * Reference: LOAN_SYNC_IMPLEMENTATION.md + * + * Call after successful repayment transaction to sync backend database + */ + async notifyLoanRepayment(request: { + txHash: string; + positionId: string; + repaymentAmount: string; + blockNumber?: string; + }): Promise<{ + success: boolean; + message: string; + position: any; + }> { + try { + console.log('📢 Notifying backend of loan repayment:', request); + + const response = await this.fetchWithTimeout( + `${this.baseURL}/solvency/loan/repay-notify`, + { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify(request), + }, + 60000 // 60 second timeout + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to notify loan repayment'); + } + + const data = await response.json(); + console.log('✅ Loan repayment notification successful:', data); + return data; + } catch (error: any) { + console.error('❌ Error notifying loan repayment:', error); + throw error; + } + } + } export const solvencyService = new SolvencyService(); diff --git a/src/pages/borrow/components/DirectBorrowModal.tsx b/src/pages/borrow/components/DirectBorrowModal.tsx index 7d22d5b..09cf31f 100644 --- a/src/pages/borrow/components/DirectBorrowModal.tsx +++ b/src/pages/borrow/components/DirectBorrowModal.tsx @@ -141,20 +141,23 @@ export const DirectBorrowModal = ({ console.log('✅ Borrow successful:', borrowResult.txHash); setTxHash(borrowResult.txHash!); - // Step 5: Sync position with backend + // Step 5: Notify backend of loan borrow setStep('syncing'); - console.log('🔄 Syncing position with backend...'); + console.log('🔄 Notifying backend of loan borrow...'); try { - await solvencyService.syncPosition({ - positionId: position.positionId.toString(), + await solvencyService.notifyLoanBorrow({ txHash: borrowResult.txHash!, - blockNumber: borrowResult.blockNumber!, + positionId: position.positionId.toString(), + borrowAmount: amountWei.toString(), + loanDuration: loanDuration.toString(), + numberOfInstallments: numberOfInstallments.toString(), + blockNumber: borrowResult.blockNumber?.toString(), }); - console.log('✅ Position synced with backend'); + console.log('✅ Backend notified of loan borrow'); } catch (syncError) { // Non-blocking: Events will still sync it automatically - console.warn('⚠️ Manual sync failed (events will auto-sync):', syncError); + console.warn('⚠️ Manual notification failed (events will auto-sync):', syncError); } // Success! diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index 65e01ee..16707a2 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -161,10 +161,13 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: B throw new Error(borrowResult.error || 'Borrow transaction failed'); } - await solvencyService.syncPosition({ - positionId: String(selectedPosition.positionId), + await solvencyService.notifyLoanBorrow({ txHash: borrowResult.txHash!, - blockNumber: borrowResult.blockNumber!, + positionId: String(selectedPosition.positionId), + borrowAmount: amountWei.toString(), + loanDuration: loanDuration.toString(), + numberOfInstallments: installments.toString(), + blockNumber: borrowResult.blockNumber?.toString(), }); onSuccess(); diff --git a/src/pages/marketplace/auction/AuctionDetails.page.tsx b/src/pages/marketplace/auction/AuctionDetails.page.tsx index b43e753..fe1c211 100644 --- a/src/pages/marketplace/auction/AuctionDetails.page.tsx +++ b/src/pages/marketplace/auction/AuctionDetails.page.tsx @@ -59,7 +59,11 @@ const AuctionDetailsPage = () => { const checkForExistingBids = async () => { try { const myBids = await marketplaceService.getUserBids(); - const hasBid = myBids.some(bid => bid.assetId === assetId && bid.status === 'PENDING'); + // Check for any active bid (PENDING, PLACED, or FINALIZED) for this asset + const hasBid = myBids.some(bid => + bid.assetId === assetId && + (bid.status === 'PENDING' || bid.status === 'PLACED' || bid.status === 'FINALIZED') + ); setHasAlreadyBidded(hasBid); } catch (error) { diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 0297205..3ee9d13 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -70,7 +70,7 @@ const PortfolioPage = () => { if (!address) return; setIsLoadingMyLoans(true); try { - const response = await solvencyService.getMyPositions('ACTIVE', 100, 0); + const response = await solvencyService.getMyPositions(); const loans = response.positions.filter(p => parseFloat(p.usdcBorrowed) > 0); setMyLoans(loans); } catch (err) { diff --git a/src/types/solvency.types.ts b/src/types/solvency.types.ts index 48d0c60..c90ee63 100644 --- a/src/types/solvency.types.ts +++ b/src/types/solvency.types.ts @@ -268,23 +268,31 @@ export interface TokenApprovalState { */ export interface Position { positionId: number; - collateralToken: { + userAddress?: string; // Address of the user + collateralToken?: { address: string; symbol: string; name: string; type: string; // "RWA" or "PRIVATE_ASSET" }; collateralAmount: string; // e.g., "90000000000000000000" (18 decimals) - tokenValueUSD: string; // e.g., "76500000000" (6 decimals) - usdcBorrowed: string; // e.g., "50000000000" (6 decimals) + tokenValueUSD?: string; // e.g., "76500000000" (6 decimals) + usdcBorrowed?: string; // Legacy field, prefer 'borrowed' + borrowed?: string; // e.g., "50000000000" (6 decimals) outstandingDebt: string; // e.g., "50041100000" (6 decimals) - healthFactor: number; // e.g., 15300 (representing 153.00%) - healthStatus: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" + healthFactor: number; // e.g., 15300 (basis points) or 2.5 (direct ratio) + healthStatus?: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" status: string; // "ACTIVE", "CLOSED", "LIQUIDATED", "SETTLED", "REPAID" - maxBorrowCapacity: string; // e.g., "53550000000" (6 decimals) + maxBorrowCapacity?: string; // e.g., "53550000000" (6 decimals) missedPayments?: number; // Number of missed payments (0-3) isDefaulted?: boolean; // Whether position has been marked as defaulted - createdAt: string; + createdAt?: string; + + // New fields from backend update + oaidIssued?: boolean; + oaidTokenId?: number; + partnerLoans?: any[]; // Array of partner loans + totalPartnerDebt?: string; } /** From 2eaae9b39b1571469414a238b7c3c7a137ff16b7 Mon Sep 17 00:00:00 2001 From: Dead-Bytes Date: Sun, 11 Jan 2026 11:17:33 +0530 Subject: [PATCH 15/65] "fixes" --- src/components/portfolio/MyLoansTable.tsx | 4 ++-- src/components/portfolio/RepayLoanModal.tsx | 2 +- src/lib/api/solvency-contract.service.ts | 22 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 80a2376..203a052 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -298,12 +298,12 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
- {position.collateralToken.symbol.substring(0, 2).toUpperCase()} + {position.collateralToken?.symbol ? position.collateralToken.symbol.substring(0, 2).toUpperCase() : 'TK'}
- {position.collateralToken.symbol} + {position.collateralToken?.symbol || 'Unknown Token'}
{formatCollateralAmount(position.collateralAmount, 18)} tokens • {formatUSD(position.tokenValueUSD)} diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 2f7543b..50286d4 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -214,7 +214,7 @@ export const RepayLoanModal = ({
Position #{position.positionId} - {position.collateralToken.symbol} + {position.collateralToken?.symbol || 'Unknown Token'}
diff --git a/src/lib/api/solvency-contract.service.ts b/src/lib/api/solvency-contract.service.ts index 9358b4e..ae90c28 100644 --- a/src/lib/api/solvency-contract.service.ts +++ b/src/lib/api/solvency-contract.service.ts @@ -450,6 +450,9 @@ class SolvencyContractService { numberOfInstallments }); + console.log('⏳ WAITING FOR USER: Please approve the transaction in your wallet!'); + console.log(' Check MetaMask or your wallet extension...'); + const tx = await vault.borrowUSDC(positionId, amount, loanDuration, numberOfInstallments); console.log(`✅ Step 4: Transaction submitted successfully!`); @@ -498,6 +501,25 @@ class SolvencyContractService { console.error(' Error code:', error.code); if (error.reason) console.error(' Error reason:', error.reason); if (error.data) console.error(' Error data:', error.data); + + // Check for user rejection + if (error.code === 'ACTION_REJECTED' || error.code === 4001) { + console.log('👤 User rejected the transaction in wallet'); + return { + success: false, + error: 'Transaction rejected by user', + }; + } + + // Check for insufficient funds + if (error.code === 'INSUFFICIENT_FUNDS') { + console.log('💰 Insufficient funds for transaction'); + return { + success: false, + error: 'Insufficient funds to complete transaction', + }; + } + return { success: false, error: error.message || 'Borrow transaction failed', From 28f4dc70c8fb1ad870da369496988e59932ad680 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 13:01:44 +0530 Subject: [PATCH 16/65] fixed the myloanstable --- src/components/portfolio/MyLoansTable.tsx | 735 ++++++++++++-------- src/components/portfolio/RepayLoanModal.tsx | 21 +- src/constants/solvency.constants.ts | 4 +- src/lib/api/solvency-contract.service.ts | 2 +- 4 files changed, 459 insertions(+), 303 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 80a2376..46d1420 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -1,21 +1,21 @@ /** - * My Loans Table - Enhanced with Loan Schedule & Repayment + * My Loans Table - Redesigned to match MyAssetsTable * * ✅ SPECIFICATION COMPLIANT * Reference: Solvency Vault End-to-End Flow - Sections 5, 6, 7 * * Features: - * - Expandable rows to show loan schedule (like My Assets table) - * - Calls GET /solvency/position/:id/schedule on expand - * - Shows: loan duration, installments, next payment, missed payments, full schedule - * - Repay button pre-fills next unpaid installment - * - Filters: All, Healthy, At Risk, Critical - * - Sort: Date, Health Factor, Debt Amount + * - Table layout matching MyAssetsTable design + * - Expandable rows to show loan schedule details + * - Proper health factor display (Infinite ∞ for no debt) + * - Defaulted loan visual indicators + * - Overdue payment warnings + * - Filters: All, Healthy, At Risk, Critical, Defaulted */ import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ChevronDown, ChevronUp, Wallet, ArrowUpDown, Calendar, AlertCircle } from 'lucide-react'; +import { ChevronDown, ChevronUp, Wallet, Filter, Calendar, AlertCircle, Info, Clock, DollarSign } from 'lucide-react'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; import { format } from 'date-fns'; @@ -46,12 +46,30 @@ const formatCollateralAmount = (value: string, decimals: number = 18) => { return num.toFixed(2); }; -// Format health factor (value like 15300 = 153.00%) +// Format health factor (value like 15300 = 153.00%, or 2147483647 for no debt) const formatHealthFactor = (value: number) => { + // MAX_INT means no debt - show as infinite/perfect health + if (value >= 2000000) return 'N/A (No Debt)'; return `${(value / 100).toFixed(2)}%`; }; +// Derive token symbol from address (simple heuristic) +const getTokenSymbol = (address: string) => { + // You can maintain a map of known addresses or derive from first/last chars + const shortAddr = address.slice(2, 8).toUpperCase(); + return `TKN-${shortAddr}`; +}; + +// Calculate outstanding debt from usdcBorrowed and totalPartnerDebt +const getOutstandingDebt = (position: Position): string => { + const borrowed = parseFloat(position.usdcBorrowed || '0'); + const partnerDebt = parseFloat(position.totalPartnerDebt || '0'); + return (borrowed + partnerDebt).toString(); +}; + const getHealthColor = (healthFactor: number) => { + // MAX_INT (2147483647) means no debt - perfectly healthy + if (healthFactor >= 2000000) return 'text-[#10B981]'; if (healthFactor >= 15000) return 'text-[#10B981]'; if (healthFactor >= 12000) return 'text-[#F59E0B]'; return 'text-[#EF4444]'; @@ -70,8 +88,13 @@ const getHealthBadgeColor = (healthStatus: string) => { } }; -type FilterType = 'all' | 'healthy' | 'warning' | 'critical'; -type SortType = 'date' | 'health' | 'debt'; +// Check if payment is overdue +const isPaymentOverdue = (nextPaymentDueDate?: string): boolean => { + if (!nextPaymentDueDate) return false; + return new Date(nextPaymentDueDate) < new Date(); +}; + +type FilterType = 'all' | 'healthy' | 'warning' | 'critical' | 'defaulted'; interface LoanSchedule { loanDuration: number; @@ -91,50 +114,45 @@ interface LoanSchedule { export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTableProps) => { const navigate = useNavigate(); const [activeFilter, setActiveFilter] = useState('all'); - const [sortBy, setSortBy] = useState('date'); - const [expandedPositionId, setExpandedPositionId] = useState(null); - const [scheduleData, setScheduleData] = useState>({}); + const [expandedRows, setExpandedRows] = useState>(new Set()); + const [scheduleData, setScheduleData] = useState>({});; const [loadingSchedule, setLoadingSchedule] = useState>({}); const [showRepayModal, setShowRepayModal] = useState(false); const [selectedPosition, setSelectedPosition] = useState(null); - // Filter and sort positions - const filteredAndSortedPositions = useMemo(() => { + // Filter positions + const filteredPositions = useMemo(() => { let filtered = [...positions]; if (activeFilter !== 'all') { filtered = filtered.filter(p => { - if (activeFilter === 'healthy') return p.healthStatus === 'HEALTHY'; + if (activeFilter === 'healthy') return p.healthStatus === 'HEALTHY' && !p.isDefaulted; if (activeFilter === 'warning') return p.healthStatus === 'WARNING'; if (activeFilter === 'critical') return p.healthStatus === 'CRITICAL'; + if (activeFilter === 'defaulted') return p.isDefaulted; return true; }); } - filtered.sort((a, b) => { - if (sortBy === 'date') { - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - } - if (sortBy === 'health') { - return a.healthFactor - b.healthFactor; - } - if (sortBy === 'debt') { - return parseFloat(b.outstandingDebt) - parseFloat(a.outstandingDebt); - } - return 0; - }); + // Sort by creation date (newest first) + filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); return filtered; - }, [positions, activeFilter, sortBy]); + }, [positions, activeFilter]); // Toggle expanded row and load schedule - const handleToggleExpand = async (positionId: number) => { - if (expandedPositionId === positionId) { - setExpandedPositionId(null); + const toggleRowExpansion = async (positionId: number, e: React.MouseEvent) => { + e.stopPropagation(); + const newExpanded = new Set(expandedRows); + + if (newExpanded.has(positionId)) { + newExpanded.delete(positionId); + setExpandedRows(newExpanded); return; } - setExpandedPositionId(positionId); + newExpanded.add(positionId); + setExpandedRows(newExpanded); // Load schedule if not already loaded if (!scheduleData[positionId] && !loadingSchedule[positionId]) { @@ -150,7 +168,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr } }; - const handleRepayClick = (position: Position) => { + const handleRepayClick = (position: Position, e: React.MouseEvent) => { + e.stopPropagation(); setSelectedPosition(position); setShowRepayModal(true); }; @@ -177,297 +196,421 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr if (positions.length === 0) { return ( -
-
- +
+
+
+ +
+

No active loans yet

+
-

- No Active Loans -

-

- You haven't borrowed against your assets yet. Start borrowing USDC using your RWA tokens as collateral. -

-
); } return ( -
- {/* Filters and Sort */} -
+
+ {/* Filter Bar */} +
- - - - -
- -
- - + + Filter: +
+ + + + + +
+ {activeFilter !== 'all' && ( + + ({filteredPositions.length} of {positions.length}) + + )}
- {/* Card Grid */} - {filteredAndSortedPositions.length === 0 ? ( -
-

No loans match the selected filter.

+ {/* Table */} + {filteredPositions.length === 0 ? ( +
+

No loans match the selected filter.

) : ( -
- {filteredAndSortedPositions.map((position) => { - const isExpanded = expandedPositionId === position.positionId; - const schedule = scheduleData[position.positionId]; - const isLoadingSchedule = loadingSchedule[position.positionId]; - - return ( -
- {/* Main Card Content */} -
-
- {/* Left: Position Info */} -
-
-
- Position - #{position.positionId} + + + + + + + + + + + + + + + + {filteredPositions?.map((position, index) => { + const isExpanded = expandedRows.has(position.positionId); + const schedule = scheduleData[position.positionId]; + const isLoadingSchedule = loadingSchedule[position.positionId]; + const isOverdue = isPaymentOverdue(position.nextPaymentDueDate); + const outstandingDebt = parseFloat(getOutstandingDebt(position)); + const hasDebt = outstandingDebt > 0; + + return ( + <> + toggleRowExpansion(position.positionId, e)} + > + {/* Expand Icon */} + + + {/* Position ID */} + + + {/* Collateral Type */} + + + {/* Collateral Amount */} + - {/* Collateral */} -
-
- - {position.collateralToken.symbol.substring(0, 2).toUpperCase()} - -
-
-
- {position.collateralToken.symbol} -
-
- {formatCollateralAmount(position.collateralAmount, 18)} tokens • {formatUSD(position.tokenValueUSD)} -
-
+ {/* USDC Borrowed */} +
- {/* Debt & Health */} -
-
-
Outstanding Debt
-
- {formatUSD(position.outstandingDebt)} -
-
-
-
Health Factor
-
- {formatHealthFactor(position.healthFactor)} -
-
+ {/* Health Factor */} +
+ + {/* Status */} + + + {/* Next Payment */} + + + {/* Actions */} + + + + {/* Expanded Detail Row */} + {isExpanded && ( + + + + )} + + ); + })} + +
+ Position + + Collateral Type + + Collateral Amount + + USDC Borrowed + + Health Factor + + Status + + Next Payment + + Actions +
+ + +
+
+ Position #{position.positionId} + {position.isDefaulted && ( +
+ +
+ Defaulted: 3+ missed payments +
+
+ )}
- - {position.healthStatus} +
+ {getTokenSymbol(position.collateralTokenAddress)} +
+
+
+ + {position.collateralTokenType} + + +
+ + {formatCollateralAmount(position.collateralAmount, 18)} + + + {formatUSD(position.tokenValueUSD)}
+
+
+ {hasDebt ? formatUSD(getOutstandingDebt(position)) : ( + No Debt + )}
+
+
+ {formatHealthFactor(position.currentHealthFactor)}
- - - {/* Right: Actions */} -
- -
+ - {isExpanded ? ( - <> - - Hide Schedule - - ) : ( - <> - - View Schedule - - )} - - - - - {/* Date */} -
- Created {format(new Date(position.createdAt), 'MMM d, yyyy')} -
- - - {/* Expanded Schedule Section */} - {isExpanded && ( -
- {isLoadingSchedule ? ( -
-
-
- ) : schedule ? ( -
-

- - Repayment Schedule -

- - {/* Schedule Summary */} -
-
-
Installments Paid
-
- {schedule.installmentsPaid} / {schedule.numberOfInstallments} -
-
-
-
Missed Payments
-
- {schedule.missedPayments} -
-
-
-
Next Payment Due
-
- {new Date(schedule.nextPaymentDue * 1000) > new Date() - ? format(new Date(schedule.nextPaymentDue * 1000), 'MMM d, yyyy') - : OVERDUE - } -
-
-
-
Payment Interval
-
- {Math.floor(schedule.installmentInterval / 86400)} days -
+ {position.healthStatus} + +
+ {position.nextPaymentDueDate && hasDebt ? ( +
+
+ + {format(new Date(position.nextPaymentDueDate), 'MMM d, yyyy')}
+ {isOverdue && ( + OVERDUE + )}
+ ) : ( + N/A + )} +
+ {hasDebt && !position.isDefaulted ? ( + + ) : ( + + {position.isDefaulted ? 'Defaulted' : 'No Action'} + + )} +
+
+ {isLoadingSchedule ? ( +
+
+
+ ) : schedule ? ( +
+ {/* Loan Details Header */} +
+ {/* LTV Ratio */} +
+
+ + LTV Ratio +
+
+ {position.initialLTV ? (position.initialLTV / 100).toFixed(0) : 'N/A'}% +
+
- {/* Installment List */} -
- {schedule.installments.map((installment) => ( -
-
-
- Installment #{installment.installmentNumber} + {/* Total Installments */} +
+
+ + Installments +
+
+ {schedule.installmentsPaid} / {schedule.numberOfInstallments} +
-
- Due: {format(new Date(installment.dueDate * 1000), 'MMM d, yyyy')} + + {/* Missed Payments */} +
+
+ + Missed +
+
+ {schedule.missedPayments} +
+
+ + {/* Payment Interval */} +
+
+ + Interval +
+
+ {Math.floor(schedule.installmentInterval / 86400)}d +
-
-
- {formatUSD(installment.amount)} + + {/* Repayment Schedule */} +
+

+ + Repayment Schedule +

+ +
+ {schedule.installments?.map((installment) => ( +
+
+
+ #{installment.installmentNumber} +
+
+ Due: {format(new Date(installment.dueDate * 1000), 'MMM d, yyyy')} +
+
+
+
+ {formatUSD(installment.amount)} +
+ + {installment.status} + +
+
+ ))}
- - {installment.status} - + + {schedule.missedPayments > 0 && ( +
+ +
+ Warning: You have {schedule.missedPayments} missed payment + {schedule.missedPayments > 1 ? 's' : ''}. Please repay immediately to avoid liquidation. +
+
+ )}
-
- ))} -
- {schedule.missedPayments > 0 && ( -
- -
- You have {schedule.missedPayments} missed payment{schedule.missedPayments > 1 ? 's' : ''}. - Please repay immediately to avoid liquidation. + {/* Transaction Info */} +
+
+ +
+ Created: + + {format(new Date(position.createdAt), 'MMM d, yyyy HH:mm')} + +
+
+
-
- )} -
- ) : ( -
- No repayment schedule available for this position. -
- )} -
- )} -
- ); - })} -
+ ) : ( +
+ No repayment schedule available for this position. +
+ )} +
+
)} {/* Repay Modal */} diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 988121d..a84c432 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -51,6 +51,19 @@ const formatUSD = (value: string | number) => { }).format(num); }; +// Derive token symbol from address +const getTokenSymbol = (address: string) => { + const shortAddr = address.slice(2, 8).toUpperCase(); + return `TKN-${shortAddr}`; +}; + +// Calculate outstanding debt +const getOutstandingDebt = (position: Position): string => { + const borrowed = parseFloat(position.usdcBorrowed || '0'); + const partnerDebt = parseFloat(position.totalPartnerDebt || '0'); + return (borrowed + partnerDebt).toString(); +}; + export const RepayLoanModal = ({ isOpen, onClose, @@ -65,7 +78,7 @@ export const RepayLoanModal = ({ const [success, setSuccess] = useState(false); const [currentStep, setCurrentStep] = useState<'input' | 'approving' | 'repaying' | 'syncing'>('input'); - const outstandingDebt = parseFloat(position.outstandingDebt) / 1e6; + const outstandingDebt = parseFloat(getOutstandingDebt(position)) / 1e6; // Calculate next installment amount const nextInstallmentAmount = useMemo(() => { @@ -213,13 +226,13 @@ export const RepayLoanModal = ({
Position #{position.positionId} - {position.collateralToken.symbol} + {getTokenSymbol(position.collateralTokenAddress)}
Outstanding Debt - {formatUSD(position.outstandingDebt)} + {formatUSD(getOutstandingDebt(position))}
@@ -262,7 +275,7 @@ export const RepayLoanModal = ({ {!isAmountValid && parseFloat(repayAmount) > 0 && (

{parseFloat(repayAmount) > outstandingDebt - ? `Amount exceeds outstanding debt of ${formatUSD(position.outstandingDebt)}` + ? `Amount exceeds outstanding debt of ${formatUSD(getOutstandingDebt(position))}` : 'Please enter a valid amount'}

)} diff --git a/src/constants/solvency.constants.ts b/src/constants/solvency.constants.ts index 7e71ba5..46638ee 100644 --- a/src/constants/solvency.constants.ts +++ b/src/constants/solvency.constants.ts @@ -79,5 +79,5 @@ export const MIN_BORROW_AMOUNT = 100; // Minimum 100 USDC export const MIN_DEPOSIT_AMOUNT = 0.01; // Minimum deposit amount in tokens // Contract addresses (to be configured per environment) -export const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x0849B8d12Ac2a7Fcab4FAe8a46154e9778579493'; -export const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || ''; \ No newline at end of file +export const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x3b3d70Fe12076f30E9999Fd65feC6C6DeB47B5eF'; +export const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x26Da2F1a2de3295302Fd95eBA1A183dc8Ffd77a3'; \ No newline at end of file diff --git a/src/lib/api/solvency-contract.service.ts b/src/lib/api/solvency-contract.service.ts index f888096..5d2da8b 100644 --- a/src/lib/api/solvency-contract.service.ts +++ b/src/lib/api/solvency-contract.service.ts @@ -10,7 +10,7 @@ import { ethers } from 'ethers'; // Contract addresses from environment const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x3b3d70Fe12076f30E9999Fd65feC6C6DeB47B5eF'; -const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x9A54Bad93a00Bf1232D4e636f5e53055Dc0b8238'; +const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x26Da2F1a2de3295302Fd95eBA1A183dc8Ffd77a3'; // Solvency Vault ABI - ✅ VERIFIED from deposit-to-vaultsolvency.js lines 115-121 // Updated borrowUSDC to include loanDuration and numberOfInstallments per COMPLETE_LOAN.md From 6c2c207dcf41890d6868c906cdbb1a7b5adb880b Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 13:13:48 +0530 Subject: [PATCH 17/65] repay modal correctied --- src/components/portfolio/MyLoansTable.tsx | 10 +++-- src/components/portfolio/RepayLoanModal.tsx | 4 +- src/pages/portfolio/Portfolio.page.tsx | 8 ++-- src/types/solvency.types.ts | 44 +++++++++++++++------ 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 46d1420..9930d5c 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -15,6 +15,7 @@ import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { createPortal } from 'react-dom'; import { ChevronDown, ChevronUp, Wallet, Filter, Calendar, AlertCircle, Info, Clock, DollarSign } from 'lucide-react'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; @@ -439,7 +440,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {/* Actions */}
- {hasDebt && !position.isDefaulted ? ( + {!position.isDefaulted ? (
)} - {/* Repay Modal */} - {selectedPosition && ( + {/* Repay Modal - Rendered at document body level using Portal */} + {selectedPosition && createPortal( { @@ -624,7 +625,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr onSuccess={handleRepaySuccess} position={selectedPosition} schedule={scheduleData[selectedPosition.positionId]} - /> + />, + document.body )}
); diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index a84c432..5ddc0d9 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -191,8 +191,8 @@ export const RepayLoanModal = ({ if (!isOpen) return null; return ( -
-
+
+
{/* Header */}

Repay Loan

diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 0297205..772be8c 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -70,16 +70,16 @@ const PortfolioPage = () => { if (!address) return; setIsLoadingMyLoans(true); try { - const response = await solvencyService.getMyPositions('ACTIVE', 100, 0); - const loans = response.positions.filter(p => parseFloat(p.usdcBorrowed) > 0); - setMyLoans(loans); + const response = await solvencyService.getMyPositions(); + console.log("loans find", response); + setMyLoans(response.positions); } catch (err) { console.error("Error fetching solvency loans:", err); showError("Failed to fetch loans", "Could not retrieve your loan positions."); } finally { setIsLoadingMyLoans(false); } - }, [address, showError]); + }, []); // Filtered data based on search term diff --git a/src/types/solvency.types.ts b/src/types/solvency.types.ts index 48d0c60..5be205c 100644 --- a/src/types/solvency.types.ts +++ b/src/types/solvency.types.ts @@ -268,23 +268,41 @@ export interface TokenApprovalState { */ export interface Position { positionId: number; - collateralToken: { + collateralTokenAddress: string; // Token contract address + collateralTokenType: string; // "RWA" or "PRIVATE_ASSET" + collateralAmount: string; // e.g., "90000000000000000000" (18 decimals) + tokenValueUSD: string; // e.g., "76500000000" (6 decimals) + usdcBorrowed: string; // e.g., "50000000000" (6 decimals) + totalPartnerDebt: string; // e.g., "0" (6 decimals) + totalRepaid: string; // e.g., "0" (6 decimals) + initialLTV: number; // e.g., 6000 (representing 60.00%) + currentHealthFactor: number; // e.g., 15300 (representing 153.00%) or 2147483647 for no debt + healthStatus: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" + status: string; // "ACTIVE", "CLOSED", "LIQUIDATED", "SETTLED", "REPAID" + loanDuration: number; // Duration in seconds + numberOfInstallments: number; // Number of installments + installmentInterval: number; // Interval in seconds + installmentsPaid: number; // Number of installments paid + missedPayments: number; // Number of missed payments (0-3) + isDefaulted: boolean; // Whether position has been marked as defaulted + oaidCreditIssued: boolean; // Whether OAID credit was issued + repaymentSchedule: any[]; // Repayment schedule array + nextPaymentDueDate?: string; // Optional next payment due date + depositTxHash: string; // Deposit transaction hash + depositBlockNumber: number; // Deposit block number + createdAt: string; + updatedAt: string; + + // Optional legacy fields for backward compatibility + collateralToken?: { address: string; symbol: string; name: string; - type: string; // "RWA" or "PRIVATE_ASSET" + type: string; }; - collateralAmount: string; // e.g., "90000000000000000000" (18 decimals) - tokenValueUSD: string; // e.g., "76500000000" (6 decimals) - usdcBorrowed: string; // e.g., "50000000000" (6 decimals) - outstandingDebt: string; // e.g., "50041100000" (6 decimals) - healthFactor: number; // e.g., 15300 (representing 153.00%) - healthStatus: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" - status: string; // "ACTIVE", "CLOSED", "LIQUIDATED", "SETTLED", "REPAID" - maxBorrowCapacity: string; // e.g., "53550000000" (6 decimals) - missedPayments?: number; // Number of missed payments (0-3) - isDefaulted?: boolean; // Whether position has been marked as defaulted - createdAt: string; + outstandingDebt?: string; // Computed from usdcBorrowed + totalPartnerDebt + healthFactor?: number; // Alias for currentHealthFactor + maxBorrowCapacity?: string; // Optional max borrow capacity } /** From 2708bf9178e7aeedadb1c46df5aa13d50ca1bffd Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 13:14:10 +0530 Subject: [PATCH 18/65] corrected type --- src/lib/api/solvency.service.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/lib/api/solvency.service.ts b/src/lib/api/solvency.service.ts index 79ad1ab..6c50b90 100644 --- a/src/lib/api/solvency.service.ts +++ b/src/lib/api/solvency.service.ts @@ -115,18 +115,12 @@ class SolvencyService extends BaseService { * meta: { total, limit, offset } * } */ - async getMyPositions(status: string = 'ACTIVE', limit: number = 20, offset: number = 0): Promise { + async getMyPositions(): Promise { try { console.log('📊 Fetching user positions...'); - const params = new URLSearchParams({ - status, - limit: limit.toString(), - offset: offset.toString(), - }); - const response = await this.fetchWithTimeout( - `${this.baseURL}/solvency/positions/my?${params}`, + `${this.baseURL}/solvency/positions/my`, { method: 'GET', headers: this.getAuthHeaders(), @@ -139,7 +133,7 @@ class SolvencyService extends BaseService { } const data: GetPositionsResponse = await response.json(); - console.log('✅ Positions received:', data); + console.log('✅ Loan Positions received:', data); return data; } catch (error: any) { console.error('❌ Error fetching positions:', error); From 2e753cfb143dfe01f232e32a78fd3e1a27605403 Mon Sep 17 00:00:00 2001 From: Dead-Bytes Date: Sun, 11 Jan 2026 13:15:34 +0530 Subject: [PATCH 19/65] "repay fix" --- src/components/portfolio/RepayLoanModal.tsx | 87 +++++++++++++++++---- src/lib/api/solvency-contract.service.ts | 82 ++++++++++++++----- 2 files changed, 133 insertions(+), 36 deletions(-) diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 50286d4..586a3e3 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -64,8 +64,32 @@ export const RepayLoanModal = ({ const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [currentStep, setCurrentStep] = useState<'input' | 'approving' | 'repaying' | 'syncing'>('input'); + const [actualDebt, setActualDebt] = useState(null); + const [isFetchingDebt, setIsFetchingDebt] = useState(false); - const outstandingDebt = parseFloat(position.outstandingDebt) / 1e6; + const outstandingDebt = actualDebt ? parseFloat(ethers.formatUnits(actualDebt, 6)) : parseFloat(position.outstandingDebt) / 1e6; + + // Fetch actual outstanding debt when modal opens + useEffect(() => { + if (isOpen) { + const fetchDebt = async () => { + setIsFetchingDebt(true); + try { + console.log('📊 Fetching actual outstanding debt from chain...'); + const debt = await solvencyContractService.getOutstandingDebt(position.positionId); + setActualDebt(debt); + console.log(`✅ Fetched debt: $${ethers.formatUnits(debt, 6)} USDC`); + } catch (err) { + console.error('Failed to fetch debt:', err); + // Fall back to position's outstanding debt + setActualDebt(null); + } finally { + setIsFetchingDebt(false); + } + }; + fetchDebt(); + } + }, [isOpen, position.positionId]); // Calculate next installment amount const nextInstallmentAmount = useMemo(() => { @@ -96,6 +120,7 @@ export const RepayLoanModal = ({ setCurrentStep('input'); setIsApproving(false); setIsRepaying(false); + setActualDebt(null); } }, [isOpen]); @@ -114,31 +139,50 @@ export const RepayLoanModal = ({ setIsApproving(true); try { - const amountWei = ethers.parseUnits(repayAmount, 6); + let amountWei = ethers.parseUnits(repayAmount, 6); + + // Step 0: Fetch actual outstanding debt and cap repayment amount + // Per repay-solvency-loan.js: Prevent "Amount exceeds debt" error + console.log('📊 Checking outstanding debt...'); + const actualDebtWei = await solvencyContractService.getOutstandingDebt(position.positionId); + + if (actualDebtWei === 0n) { + setError('No outstanding debt for this position!'); + setCurrentStep('input'); + setIsApproving(false); + return; + } + + // Cap repayment to actual debt + if (amountWei > actualDebtWei) { + console.log(`⚠️ Repayment amount ($${repayAmount}) exceeds actual debt ($${ethers.formatUnits(actualDebtWei, 6)})`); + console.log(' Capping repayment to exact outstanding debt...'); + amountWei = actualDebtWei; + } + + console.log(`💰 Final Repayment Amount: $${ethers.formatUnits(amountWei, 6)} USDC`); - // Step 1: Approve USDC for SeniorPool - // Per COMPLETE_LOAN.md: Approve SeniorPool to spend USDC, not Vault - console.log('📝 Approving USDC for SeniorPool...'); + // Step 1: Approve USDC for Vault + console.log('📝 Approving USDC for Vault...'); const approvalResult = await solvencyContractService.approveUSDCForSeniorPool(amountWei); if (!approvalResult.success) { - throw new Error(approvalResult.error || 'USDC approval for SeniorPool failed'); + throw new Error(approvalResult.error || 'USDC approval for Vault failed'); } setIsApproving(false); setCurrentStep('repaying'); setIsRepaying(true); - // Step 2: Direct wallet call to SeniorPool.repayLoan() - // Per COMPLETE_LOAN.md: Call SeniorPool directly, not backend API - console.log('💵 Repaying loan via SeniorPool...'); + // Step 2: Repay loan via Vault + console.log('💵 Repaying loan via Vault...'); const repayResult = await solvencyContractService.repayLoanViaSeniorPool( position.positionId, amountWei ); if (!repayResult.success) { - throw new Error(repayResult.error || 'Repayment via SeniorPool failed'); + throw new Error(repayResult.error || 'Repayment via Vault failed'); } setCurrentStep('syncing'); @@ -219,9 +263,16 @@ export const RepayLoanModal = ({
Outstanding Debt - - {formatUSD(position.outstandingDebt)} - + {isFetchingDebt ? ( +
+ + Loading... +
+ ) : ( + + {formatUSD(outstandingDebt)} + + )}
@@ -288,8 +339,14 @@ export const RepayLoanModal = ({ Half
-

- No Active Loans -

-

- You haven't borrowed against your assets yet. Start borrowing USDC using your RWA tokens as collateral. -

-
); } return ( -
- {/* Filters and Sort */} -
+
+ {/* Filter Bar */} +
- - - - -
- -
- - + + Filter: +
+ + + + + +
+ {activeFilter !== 'all' && ( + + ({filteredPositions.length} of {positions.length}) + + )}
- {/* Card Grid */} - {filteredAndSortedPositions.length === 0 ? ( -
-

No loans match the selected filter.

+ {/* Table */} + {filteredPositions.length === 0 ? ( +
+

No loans match the selected filter.

) : ( -
- {filteredAndSortedPositions.map((position) => { - const isExpanded = expandedPositionId === position.positionId; - const schedule = scheduleData[position.positionId]; - const isLoadingSchedule = loadingSchedule[position.positionId]; - - return ( -
- {/* Main Card Content */} -
-
- {/* Left: Position Info */} -
-
-
- Position - #{position.positionId} + + + + + + + + + + + + + + + + {filteredPositions?.map((position, index) => { + const isExpanded = expandedRows.has(position.positionId); + const schedule = scheduleData[position.positionId]; + const isLoadingSchedule = loadingSchedule[position.positionId]; + const isOverdue = isPaymentOverdue(position.nextPaymentDueDate); + const outstandingDebt = parseFloat(getOutstandingDebt(position)); + const hasDebt = outstandingDebt > 0; + + return ( + <> + toggleRowExpansion(position.positionId, e)} + > + {/* Expand Icon */} + + + {/* Position ID */} + + + {/* Collateral Type */} + + + {/* Collateral Amount */} + - {/* Collateral */} -
-
- - {position.collateralToken?.symbol ? position.collateralToken.symbol.substring(0, 2).toUpperCase() : 'TK'} - -
-
-
- {position.collateralToken?.symbol || 'Unknown Token'} -
-
- {formatCollateralAmount(position.collateralAmount, 18)} tokens • {formatUSD(position.tokenValueUSD)} -
-
+ {/* USDC Borrowed */} +
- {/* Debt & Health */} -
-
-
Outstanding Debt
-
- {formatUSD(position.outstandingDebt)} -
-
-
-
Health Factor
-
- {formatHealthFactor(position.healthFactor)} -
-
+ {/* Health Factor */} +
+ + {/* Status */} + + + {/* Next Payment */} + + + {/* Actions */} + + + + {/* Expanded Detail Row */} + {isExpanded && ( + + + + )} + + ); + })} + +
+ Position + + Collateral Type + + Collateral Amount + + USDC Borrowed + + Health Factor + + Status + + Next Payment + + Actions +
+ + +
+
+ Position #{position.positionId} + {position.isDefaulted && ( +
+ +
+ Defaulted: 3+ missed payments +
+
+ )}
- - {position.healthStatus} +
+ {getTokenSymbol(position.collateralTokenAddress)} +
+
+
+ + {position.collateralTokenType} + + +
+ + {formatCollateralAmount(position.collateralAmount, 18)} + + + {formatUSD(position.tokenValueUSD)}
+
+
+ {hasDebt ? formatUSD(getOutstandingDebt(position)) : ( + No Debt + )}
+
+
+ {formatHealthFactor(position.currentHealthFactor)}
- - - {/* Right: Actions */} -
- -
+ - {isExpanded ? ( - <> - - Hide Schedule - - ) : ( - <> - - View Schedule - - )} - - - - - {/* Date */} -
- Created {format(new Date(position.createdAt), 'MMM d, yyyy')} -
- - - {/* Expanded Schedule Section */} - {isExpanded && ( -
- {isLoadingSchedule ? ( -
-
-
- ) : schedule ? ( -
-

- - Repayment Schedule -

- - {/* Schedule Summary */} -
-
-
Installments Paid
-
- {schedule.installmentsPaid} / {schedule.numberOfInstallments} -
-
-
-
Missed Payments
-
- {schedule.missedPayments} -
-
-
-
Next Payment Due
-
- {new Date(schedule.nextPaymentDue * 1000) > new Date() - ? format(new Date(schedule.nextPaymentDue * 1000), 'MMM d, yyyy') - : OVERDUE - } -
-
-
-
Payment Interval
-
- {Math.floor(schedule.installmentInterval / 86400)} days -
+ {position.healthStatus} + +
+ {position.nextPaymentDueDate && hasDebt ? ( +
+
+ + {format(new Date(position.nextPaymentDueDate), 'MMM d, yyyy')}
+ {isOverdue && ( + OVERDUE + )}
+ ) : ( + N/A + )} +
+ {!position.isDefaulted ? ( + + ) : ( + + {position.isDefaulted ? 'Defaulted' : 'No Action'} + + )} +
+
+ {isLoadingSchedule ? ( +
+
+
+ ) : schedule ? ( +
+ {/* Loan Details Header */} +
+ {/* LTV Ratio */} +
+
+ + LTV Ratio +
+
+ {position.initialLTV ? (position.initialLTV / 100).toFixed(0) : 'N/A'}% +
+
- {/* Installment List */} -
- {schedule.installments.map((installment) => ( -
-
-
- Installment #{installment.installmentNumber} + {/* Total Installments */} +
+
+ + Installments +
+
+ {schedule.installmentsPaid} / {schedule.numberOfInstallments} +
-
- Due: {format(new Date(installment.dueDate * 1000), 'MMM d, yyyy')} + + {/* Missed Payments */} +
+
+ + Missed +
+
+ {schedule.missedPayments} +
+
+ + {/* Payment Interval */} +
+
+ + Interval +
+
+ {Math.floor(schedule.installmentInterval / 86400)}d +
-
-
- {formatUSD(installment.amount)} + + {/* Repayment Schedule */} +
+

+ + Repayment Schedule +

+ +
+ {schedule.installments?.map((installment) => ( +
+
+
+ #{installment.installmentNumber} +
+
+ Due: {format(new Date(installment.dueDate * 1000), 'MMM d, yyyy')} +
+
+
+
+ {formatUSD(installment.amount)} +
+ + {installment.status} + +
+
+ ))}
- - {installment.status} - + + {schedule.missedPayments > 0 && ( +
+ +
+ Warning: You have {schedule.missedPayments} missed payment + {schedule.missedPayments > 1 ? 's' : ''}. Please repay immediately to avoid liquidation. +
+
+ )}
-
- ))} -
- {schedule.missedPayments > 0 && ( -
- -
- You have {schedule.missedPayments} missed payment{schedule.missedPayments > 1 ? 's' : ''}. - Please repay immediately to avoid liquidation. + {/* Transaction Info */} +
+
+ +
+ Created: + + {format(new Date(position.createdAt), 'MMM d, yyyy HH:mm')} + +
+
+
-
- )} -
- ) : ( -
- No repayment schedule available for this position. -
- )} -
- )} -
- ); - })} -
+ ) : ( +
+ No repayment schedule available for this position. +
+ )} +
+
)} - {/* Repay Modal */} - {selectedPosition && ( + {/* Repay Modal - Rendered at document body level using Portal */} + {selectedPosition && createPortal( { @@ -481,7 +625,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr onSuccess={handleRepaySuccess} position={selectedPosition} schedule={scheduleData[selectedPosition.positionId]} - /> + />, + document.body )}
); diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 586a3e3..b725c0f 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -51,6 +51,19 @@ const formatUSD = (value: string | number) => { }).format(num); }; +// Derive token symbol from address +const getTokenSymbol = (address: string) => { + const shortAddr = address.slice(2, 8).toUpperCase(); + return `TKN-${shortAddr}`; +}; + +// Calculate outstanding debt +const getOutstandingDebt = (position: Position): string => { + const borrowed = parseFloat(position.usdcBorrowed || '0'); + const partnerDebt = parseFloat(position.totalPartnerDebt || '0'); + return (borrowed + partnerDebt).toString(); +}; + export const RepayLoanModal = ({ isOpen, onClose, @@ -223,8 +236,8 @@ export const RepayLoanModal = ({ if (!isOpen) return null; return ( -
-
+
+
{/* Header */}

Repay Loan

@@ -258,7 +271,7 @@ export const RepayLoanModal = ({
Position #{position.positionId} - {position.collateralToken?.symbol || 'Unknown Token'} + {getTokenSymbol(position.collateralTokenAddress)}
@@ -314,7 +327,7 @@ export const RepayLoanModal = ({ {!isAmountValid && parseFloat(repayAmount) > 0 && (

{parseFloat(repayAmount) > outstandingDebt - ? `Amount exceeds outstanding debt of ${formatUSD(position.outstandingDebt)}` + ? `Amount exceeds outstanding debt of ${formatUSD(getOutstandingDebt(position))}` : 'Please enter a valid amount'}

)} diff --git a/src/constants/protocols.constants.ts b/src/constants/protocols.constants.ts deleted file mode 100644 index 4a45cf8..0000000 --- a/src/constants/protocols.constants.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Protocol Constants - * - * IMPORTANT: Protocols are 3RD PARTY services, not managed by our backend! - * Each protocol has its own API and integration method. - * - * In real implementation: - * - API keys would be stored in environment variables - * - Each protocol would have its own SDK/API integration - * - When user clicks "Borrow", the protocol's modal opens - * - After successful borrow, protocol notifies us via webhook - */ - -import type { Protocol as ProtocolType } from '../types/solvency.types'; - -// Re-export the type for convenience -export type Protocol = ProtocolType; - -/** - * Available protocols for borrowing against OAID credit - * These are HARDCODED in the frontend (not fetched from backend) - */ -export const SUPPORTED_PROTOCOLS: Protocol[] = [ - { - id: 'aave', - name: 'Aave', - description: 'Leading decentralized lending protocol with over $10B TVL', - logo: '/protocols/aave.svg', - apy: 3.2, - tvl: 10500000000, // $10.5B - borrowRate: 3.2, - minBorrow: 1000, // $1,000 - maxBorrow: 1000000, // $1M - isActive: true, - category: 'DeFi', - }, - { - id: 'compound', - name: 'Compound', - description: 'Algorithmic money market protocol on Ethereum', - logo: '/protocols/compound.svg', - apy: 2.8, - tvl: 3200000000, // $3.2B - borrowRate: 2.8, - minBorrow: 1000, - maxBorrow: 500000, - isActive: true, - category: 'DeFi', - }, - { - id: 'morpho', - name: 'Morpho', - description: 'Optimized lending rates through peer-to-peer matching', - logo: '/protocols/morpho.svg', - apy: 3.8, - tvl: 1800000000, // $1.8B - borrowRate: 3.8, - minBorrow: 1000, - maxBorrow: 750000, - isActive: false, // Coming soon - category: 'DeFi', - }, -]; - -/** - * Get protocol by ID - */ -export const getProtocolById = (id: string): Protocol | undefined => { - return SUPPORTED_PROTOCOLS.find(p => p.id === id); -}; - -/** - * Get active protocols only - */ -export const getActiveProtocols = (): Protocol[] => { - return SUPPORTED_PROTOCOLS.filter(p => p.isActive); -}; - -/** - * Protocol API Keys (from environment) - * In production, these would be used to integrate with protocol SDKs - */ -export const PROTOCOL_API_KEYS = { - aave: import.meta.env.VITE_AAVE_API_KEY, - compound: import.meta.env.VITE_COMPOUND_API_KEY, - morpho: import.meta.env.VITE_MORPHO_API_KEY, -}; - -/** - * Protocol integration methods - * Each protocol would have its own integration logic - */ -export const PROTOCOL_INTEGRATIONS = { - aave: { - /** - * Opens Aave's borrow modal - * In production, this would: - * 1. Initialize Aave SDK with API key - * 2. Open modal with user's OAID - * 3. Handle borrow transaction - * 4. Notify backend after success - */ - openBorrowModal: async (oaidAddress: string, amount: string) => { - console.log('🏦 Opening Aave borrow modal', { oaidAddress, amount }); - // TODO: Integrate with Aave SDK - alert('Aave integration coming soon!'); - }, - }, - - compound: { - /** - * Opens Compound's borrow modal - */ - openBorrowModal: async (oaidAddress: string, amount: string) => { - console.log('🏦 Opening Compound borrow modal', { oaidAddress, amount }); - // TODO: Integrate with Compound SDK - alert('Compound integration coming soon!'); - }, - }, - - morpho: { - /** - * Redirects to Morpho's website - */ - redirectToBorrow: (oaidAddress: string, amount: string) => { - console.log('🏦 Redirecting to Morpho', { oaidAddress, amount }); - // TODO: Build redirect URL with parameters - window.open('https://morpho.xyz', '_blank'); - }, - }, -}; diff --git a/src/constants/solvency.constants.ts b/src/constants/solvency.constants.ts index 7e71ba5..46638ee 100644 --- a/src/constants/solvency.constants.ts +++ b/src/constants/solvency.constants.ts @@ -79,5 +79,5 @@ export const MIN_BORROW_AMOUNT = 100; // Minimum 100 USDC export const MIN_DEPOSIT_AMOUNT = 0.01; // Minimum deposit amount in tokens // Contract addresses (to be configured per environment) -export const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x0849B8d12Ac2a7Fcab4FAe8a46154e9778579493'; -export const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || ''; \ No newline at end of file +export const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x3b3d70Fe12076f30E9999Fd65feC6C6DeB47B5eF'; +export const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x26Da2F1a2de3295302Fd95eBA1A183dc8Ffd77a3'; \ No newline at end of file diff --git a/src/lib/api/solvency-contract.service.ts b/src/lib/api/solvency-contract.service.ts index cb512fe..1d58e47 100644 --- a/src/lib/api/solvency-contract.service.ts +++ b/src/lib/api/solvency-contract.service.ts @@ -10,7 +10,7 @@ import { ethers } from 'ethers'; // Contract addresses from environment const VAULT_CONTRACT_ADDRESS = import.meta.env.VITE_VAULT_CONTRACT_ADDRESS || '0x3b3d70Fe12076f30E9999Fd65feC6C6DeB47B5eF'; -const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x9A54Bad93a00Bf1232D4e636f5e53055Dc0b8238'; +const USDC_CONTRACT_ADDRESS = import.meta.env.VITE_USDC_CONTRACT_ADDRESS || '0x26Da2F1a2de3295302Fd95eBA1A183dc8Ffd77a3'; // Solvency Vault ABI - ✅ VERIFIED from deposit-to-vaultsolvency.js lines 115-121 // Updated borrowUSDC to include loanDuration and numberOfInstallments per COMPLETE_LOAN.md diff --git a/src/lib/api/solvency.service.ts b/src/lib/api/solvency.service.ts index b0daa8d..249ff9d 100644 --- a/src/lib/api/solvency.service.ts +++ b/src/lib/api/solvency.service.ts @@ -133,7 +133,7 @@ class SolvencyService extends BaseService { } const data: GetPositionsResponse = await response.json(); - console.log('✅ Positions received:', data); + console.log('✅ Loan Positions received:', data); return data; } catch (error: any) { console.error('❌ Error fetching positions:', error); diff --git a/src/pages/app/borrow/Borrow.page.tsx b/src/pages/app/borrow/Borrow.page.tsx deleted file mode 100644 index 49b64ca..0000000 --- a/src/pages/app/borrow/Borrow.page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from 'react'; -import { LeverageForm } from '../../../components/leverage/LeverageForm'; -import { PositionsTable } from '../../../components/leverage/PositionsTable'; -import { PositionStats } from '../../../components/leverage/PositionStats'; -import HeroBackground from '../../landing/HeroBackground'; -import Navbar from '../../../components/common/Navbar'; -import { useLeverageStore } from '../../../stores/leverage.store'; -import type { LeveragePosition } from '../../../types/leverage.types'; -import { PositionDetailChart } from '../../../components/leverage/PositionDetailChart'; - -const BorrowPage = () => { - const { positions, isLoading } = useLeverageStore(); - const [selectedPosition, setSelectedPosition] = useState(null); - - return ( -
- - - {/* Navbar Container */} -
-
- -
-
- - {/* Main Content */} -
-
-
-

Leverage & Borrow

-

- Use your mETH as collateral to access liquidity and amplify your RWA positions. -

-
- - - -
-
- -
-
- -
-
-
-
- - {/* Position Detail Modal */} - {selectedPosition && ( - setSelectedPosition(null)} - /> - )} -
- ); -}; - -export default BorrowPage; \ No newline at end of file diff --git a/src/pages/borrow/components/BorrowModal.tsx b/src/pages/borrow/components/BorrowModal.tsx deleted file mode 100644 index 6dabf11..0000000 --- a/src/pages/borrow/components/BorrowModal.tsx +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Borrow Modal Component - * Modal for borrowing USDC through 3rd party protocols (Aave, Compound, etc.) - * - * IMPORTANT: Borrowing happens through 3rd party protocol interfaces/SDKs - * This modal shows available credit and opens the protocol's borrowing interface - */ - -import { useState, useMemo } from 'react'; -import type { Protocol, OAIDCreditLine } from '../../../types/solvency.types'; -import { formatUSD } from '../../../utils/solvency/format-credit.util'; -import { previewHealthFactorChange } from '../../../utils/solvency/health-factor.util'; -import { PROTOCOL_INTEGRATIONS } from '../../../constants/protocols.constants'; - -interface BorrowModalProps { - isOpen: boolean; - onClose: () => void; - protocol: Protocol; - creditData: OAIDCreditLine; - onSuccess: () => void; -} - -export const BorrowModal = ({ - isOpen, - onClose, - protocol, - creditData, - onSuccess, -}: BorrowModalProps) => { - const [borrowAmount, setBorrowAmount] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - - // Calculate new health factor based on borrow amount - const { newHealthFactor, healthStatus, isValid } = useMemo(() => { - if (!borrowAmount || parseFloat(borrowAmount) <= 0) { - return { - newHealthFactor: creditData.healthFactor, - healthStatus: 'current', - isValid: true, - }; - } - - const amount = parseFloat(borrowAmount); - - // Check if amount exceeds available credit - if (amount > creditData.availableCredit) { - return { - newHealthFactor: 0, - healthStatus: 'insufficient', - isValid: false, - }; - } - - // Calculate total collateral value - const totalCollateralValue = creditData.collateral.reduce( - (sum, pos) => sum + pos.valueUSD, - 0 - ); - - // Use previewHealthFactorChange to calculate new health factor after borrow - const preview = previewHealthFactorChange( - totalCollateralValue, // currentCollateralUSD - creditData.currentDebt, // currentDebtUSD - 0, // collateralChange (no change) - amount // debtChange (borrowing adds to debt) - ); - - let status = 'healthy'; - if (preview.afterAction < 110) status = 'critical'; - else if (preview.afterAction < 125) status = 'warning'; - - return { - newHealthFactor: preview.afterAction, - healthStatus: status, - isValid: preview.isSafe, - }; - }, [borrowAmount, creditData]); - - const handleBorrow = async () => { - if (!borrowAmount || parseFloat(borrowAmount) <= 0 || !isValid) { - return; - } - - setIsProcessing(true); - - try { - // Get protocol integration - const integration = PROTOCOL_INTEGRATIONS[protocol.id as keyof typeof PROTOCOL_INTEGRATIONS]; - - if (!integration) { - alert(`${protocol.name} integration coming soon! This will open ${protocol.name}'s borrowing interface.`); - onClose(); - return; - } - - // Open protocol's borrow interface - if ('openBorrowModal' in integration) { - await integration.openBorrowModal( - creditData.oaidId, - borrowAmount - ); - } else if ('redirectToBorrow' in integration) { - integration.redirectToBorrow(creditData.oaidId, borrowAmount); - } - - // After successful borrow from protocol, refresh data - onSuccess(); - onClose(); - } catch (error: any) { - console.error('Borrow error:', error); - alert(error.message || 'Failed to open borrow interface'); - } finally { - setIsProcessing(false); - } - }; - - const handleMaxClick = () => { - setBorrowAmount(creditData.availableCredit.toString()); - }; - - if (!isOpen) return null; - - return ( -
-
- {/* Header */} -
-
-

- Borrow from {protocol.name} -

-

- {protocol.description} -

-
- -
- - {/* Protocol Info */} -
-
-
-
Borrow APR
-
{protocol.borrowRate}%
-
-
-
Protocol TVL
-
- ${(protocol.tvl / 1e9).toFixed(2)}B -
-
-
-
- - {/* Available Credit */} -
-
- Available to Borrow - - {formatUSD(creditData.availableCredit)} - -
-
- Current debt: {formatUSD(creditData.currentDebt)} • - Credit limit: {formatUSD(creditData.creditLimit)} -
-
- - {/* Amount Input */} -
- -
- setBorrowAmount(e.target.value)} - placeholder="0.00" - className="w-full px-4 py-3 pr-20 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#111111] focus:border-transparent text-lg" - disabled={isProcessing} - min="0" - max={creditData.availableCredit} - step="0.01" - /> - -
- {borrowAmount && parseFloat(borrowAmount) > creditData.availableCredit && ( -

- ⚠️ Amount exceeds available credit -

- )} -
- - {/* Health Factor Preview */} - {borrowAmount && parseFloat(borrowAmount) > 0 && ( -
-
- Current Health Factor - - {creditData.healthFactor}% - -
-
- New Health Factor - - {healthStatus === 'insufficient' ? 'Insufficient Credit' : `${newHealthFactor.toFixed(2)}%`} - -
- {healthStatus === 'warning' && ( -

- ⚠️ Health factor approaching liquidation threshold (110%) -

- )} - {healthStatus === 'critical' && ( -

- ❌ This borrow would put your position at risk of liquidation -

- )} -
- )} - - {/* Important Notice */} -
-
- - - -
-

- Borrowing through {protocol.name} -

-

- This will open {protocol.name}'s interface where you can borrow against your OAID credit line. - The protocol will notify us after a successful borrow. -

-
-
-
- - {/* Actions */} -
- - -
-
-
- ); -}; diff --git a/src/pages/borrow/components/CreditSummaryCard.tsx b/src/pages/borrow/components/CreditSummaryCard.tsx deleted file mode 100644 index 47b4193..0000000 --- a/src/pages/borrow/components/CreditSummaryCard.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Credit Summary Card Component - * Displays user's credit line information and health factor - */ - -import { formatUSD, formatPercentage } from '../../../utils/solvency/format-credit.util'; -import { HealthFactorBar } from './HealthFactorBar'; -import type { OAIDCreditLine } from '../../../types/solvency.types'; - -interface CreditSummaryCardProps { - creditData: OAIDCreditLine; -} - -export const CreditSummaryCard = ({ creditData }: CreditSummaryCardProps) => { - const { - availableCredit, - creditLimit, - currentDebt, - healthFactor, - collateral, - interestRate, - totalInterestAccrued, - } = creditData; - - // Calculate total collateral value - const totalCollateralValue = collateral.reduce((sum, pos) => sum + pos.valueUSD, 0); - - // TODO: Credit utilization will be displayed in future version - // const creditUtilization = creditLimit > 0 ? (currentDebt / creditLimit) * 100 : 0; - - return ( -
- {/* Header */} -
-

Your Credit Summary

-

- Overview of your borrowing capacity and current positions -

-
- - {/* Available Credit - Large Display */} -
-
Available to Borrow
-
- {formatUSD(availableCredit)} -
-
- {formatUSD(currentDebt)} of {formatUSD(creditLimit)} used -
-
- - {/* Health Factor */} -
- -
- - {/* Stats Grid */} -
- {/* Total Collateral */} -
-
Total Collateral
-
- {formatUSD(totalCollateralValue)} -
-
- - {/* Current Debt */} -
-
Current Debt
-
- {formatUSD(currentDebt)} -
-
- - {/* Interest Rate */} -
-
Interest Rate
-
- {formatPercentage(interestRate)} APR -
-
- - {/* Interest Accrued */} -
-
Interest Accrued
-
- {formatUSD(totalInterestAccrued)} -
-
-
- - {/* Collateral Breakdown */} - {collateral.length > 0 && ( -
-
Collateral Breakdown
-
- {collateral.map((col, index) => ( -
-
-
- - {col.tokenSymbol.charAt(0)} - -
-
-
{col.tokenSymbol}
-
LTV: {col.ltvRatio}%
-
-
-
-
- {formatUSD(col.valueUSD)} -
-
- {col.amount.toLocaleString()} {col.tokenSymbol} -
-
-
- ))} -
-
- )} -
- ); -}; diff --git a/src/pages/borrow/components/HealthMonitorBanner.tsx b/src/pages/borrow/components/HealthMonitorBanner.tsx deleted file mode 100644 index 203fe9a..0000000 --- a/src/pages/borrow/components/HealthMonitorBanner.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Health Monitor Banner Component - * Warning banner shown when health factor is below healthy threshold - */ - -import { formatHealthFactor } from '../../../utils/solvency/health-factor.util'; -import { HEALTH_FACTOR_COLORS } from '../../../constants/solvency.constants'; -import type { HealthStatus } from '../../../types/solvency.types'; - -interface HealthMonitorBannerProps { - healthStatus: HealthStatus; - healthFactor: number; -} - -export const HealthMonitorBanner = ({ - healthStatus, - healthFactor, -}: HealthMonitorBannerProps) => { - const colors = HEALTH_FACTOR_COLORS[healthStatus]; - - const getMessage = () => { - switch (healthStatus) { - case 'warning': - return { - title: '⚠️ Low Health Factor', - message: 'Your health factor is getting low. Consider adding more collateral or repaying some debt to improve your account health.', - }; - case 'critical': - return { - title: '🚨 Critical Health Factor', - message: 'Your account is at risk of liquidation! Add collateral or repay debt immediately to avoid losing your assets.', - }; - default: - return { - title: 'Health Alert', - message: 'Please review your account health.', - }; - } - }; - - const { title, message } = getMessage(); - - return ( -
-
-
-

{title}

-

{message}

-
- Current Health Factor: - - {formatHealthFactor(healthFactor)} - -
-
- {healthStatus === 'critical' && ( - - )} -
-
- ); -}; diff --git a/src/pages/borrow/components/ProtocolCard.tsx b/src/pages/borrow/components/ProtocolCard.tsx deleted file mode 100644 index c5185b8..0000000 --- a/src/pages/borrow/components/ProtocolCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Protocol Card Component - * Individual protocol card with borrow button - */ - -import { formatPercentage, formatCompactNumber } from '../../../utils/solvency/format-credit.util'; -import type { Protocol } from '../../../types/solvency.types'; - -interface ProtocolCardProps { - protocol: Protocol; - onBorrow: (protocol: Protocol) => void; - disabled?: boolean; -} - -export const ProtocolCard = ({ protocol, onBorrow, disabled = false }: ProtocolCardProps) => { - return ( -
- {/* Protocol Header */} -
-
- {protocol.logo ? ( - {protocol.name} - ) : ( - {protocol.name.charAt(0)} - )} -
-
-

{protocol.name}

-

{protocol.category}

-
-
- - {/* Description */} -

- {protocol.description} -

- - {/* Stats */} -
-
-
Expected APY
-
- {formatPercentage(protocol.apy)} -
-
-
-
TVL
-
- ${formatCompactNumber(protocol.tvl)} -
-
-
- - {/* Borrow Rate */} -
- Borrow Rate - - {formatPercentage(protocol.borrowRate)} APR - -
- - {/* Borrow Button */} - -
- ); -}; diff --git a/src/pages/borrow/components/ProtocolGrid.tsx b/src/pages/borrow/components/ProtocolGrid.tsx deleted file mode 100644 index 46f3268..0000000 --- a/src/pages/borrow/components/ProtocolGrid.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Protocol Grid Component - * Displays available protocols in a responsive grid - */ - -import { ProtocolCard } from './ProtocolCard'; -import type { Protocol } from '../../../types/solvency.types'; - -interface ProtocolGridProps { - protocols: Protocol[]; - onBorrow: (protocol: Protocol) => void; - disabled?: boolean; -} - -export const ProtocolGrid = ({ protocols, onBorrow, disabled = false }: ProtocolGridProps) => { - if (protocols.length === 0) { - return ( -
-

No protocols available at this time

-
- ); - } - - return ( -
- {protocols.map((protocol) => ( - - ))} -
- ); -}; diff --git a/src/pages/borrow/hooks/useHealthMonitor.ts b/src/pages/borrow/hooks/useHealthMonitor.ts deleted file mode 100644 index bf9e64e..0000000 --- a/src/pages/borrow/hooks/useHealthMonitor.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * useHealthMonitor Hook - * Monitors health factor and provides real-time status updates - */ - -import { useState, useEffect } from 'react'; -import { getHealthStatus } from '../../../utils/solvency/health-factor.util'; -import { SOLVENCY_CONFIG } from '../../../constants/solvency.constants'; -import type { OAIDCreditLine, HealthStatus } from '../../../types/solvency.types'; - -export const useHealthMonitor = (creditData: OAIDCreditLine | null) => { - const [healthStatus, setHealthStatus] = useState('unknown'); - const [healthFactor, setHealthFactor] = useState(Infinity); - - useEffect(() => { - if (!creditData) { - setHealthStatus('unknown'); - setHealthFactor(Infinity); - return; - } - - // Update health status immediately - const updateHealth = () => { - const currentHealth = creditData.healthFactor; - const status = getHealthStatus(currentHealth); - - setHealthFactor(currentHealth); - setHealthStatus(status); - }; - - // Initial update - updateHealth(); - - // Set up interval to check health every 30 seconds - const interval = setInterval(updateHealth, SOLVENCY_CONFIG.healthMonitorInterval); - - return () => clearInterval(interval); - }, [creditData]); - - return { - healthStatus, - healthFactor, - }; -}; diff --git a/src/pages/borrow/hooks/useProtocols.ts b/src/pages/borrow/hooks/useProtocols.ts deleted file mode 100644 index 59b41b2..0000000 --- a/src/pages/borrow/hooks/useProtocols.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * useProtocols Hook - * Returns available protocols for borrowing - * - * ⚠️ IMPORTANT: Protocols are HARDCODED in frontend (not from backend API) - * Protocols are 3rd party services with their own APIs - */ - -import { useState, useEffect } from 'react'; -import { getActiveProtocols, type Protocol } from '../../../constants/protocols.constants'; - -export const useProtocols = () => { - const [protocols, setProtocols] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchProtocols = async () => { - setIsLoading(true); - setError(null); - - try { - // ✅ Protocols are hardcoded constants (not API call) - // In production, this could include runtime checks like: - // - Checking if API keys are configured - // - Validating protocol availability - // - Fetching live APR rates from protocol APIs - - await new Promise(resolve => setTimeout(resolve, 100)); // Simulate async - const activeProtocols = getActiveProtocols(); - setProtocols(activeProtocols); - } catch (err: any) { - console.error('Error loading protocols:', err); - setError(err.message || 'Failed to load protocols'); - setProtocols([]); - } finally { - setIsLoading(false); - } - }; - - fetchProtocols(); - }, []); - - return { - protocols, - isLoading, - error, - }; -}; diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 3ee9d13..772be8c 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -71,15 +71,15 @@ const PortfolioPage = () => { setIsLoadingMyLoans(true); try { const response = await solvencyService.getMyPositions(); - const loans = response.positions.filter(p => parseFloat(p.usdcBorrowed) > 0); - setMyLoans(loans); + console.log("loans find", response); + setMyLoans(response.positions); } catch (err) { console.error("Error fetching solvency loans:", err); showError("Failed to fetch loans", "Could not retrieve your loan positions."); } finally { setIsLoadingMyLoans(false); } - }, [address, showError]); + }, []); // Filtered data based on search term diff --git a/src/types/solvency.types.ts b/src/types/solvency.types.ts index c90ee63..9524dc7 100644 --- a/src/types/solvency.types.ts +++ b/src/types/solvency.types.ts @@ -268,7 +268,32 @@ export interface TokenApprovalState { */ export interface Position { positionId: number; - userAddress?: string; // Address of the user + collateralTokenAddress: string; // Token contract address + collateralTokenType: string; // "RWA" or "PRIVATE_ASSET" + collateralAmount: string; // e.g., "90000000000000000000" (18 decimals) + tokenValueUSD: string; // e.g., "76500000000" (6 decimals) + usdcBorrowed: string; // e.g., "50000000000" (6 decimals) + totalPartnerDebt: string; // e.g., "0" (6 decimals) + totalRepaid: string; // e.g., "0" (6 decimals) + initialLTV: number; // e.g., 6000 (representing 60.00%) + currentHealthFactor: number; // e.g., 15300 (representing 153.00%) or 2147483647 for no debt + healthStatus: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" + status: string; // "ACTIVE", "CLOSED", "LIQUIDATED", "SETTLED", "REPAID" + loanDuration: number; // Duration in seconds + numberOfInstallments: number; // Number of installments + installmentInterval: number; // Interval in seconds + installmentsPaid: number; // Number of installments paid + missedPayments: number; // Number of missed payments (0-3) + isDefaulted: boolean; // Whether position has been marked as defaulted + oaidCreditIssued: boolean; // Whether OAID credit was issued + repaymentSchedule: any[]; // Repayment schedule array + nextPaymentDueDate?: string; // Optional next payment due date + depositTxHash: string; // Deposit transaction hash + depositBlockNumber: number; // Deposit block number + createdAt: string; + updatedAt: string; + + // Optional legacy fields for backward compatibility collateralToken?: { address: string; symbol: string; @@ -276,23 +301,16 @@ export interface Position { type: string; // "RWA" or "PRIVATE_ASSET" }; collateralAmount: string; // e.g., "90000000000000000000" (18 decimals) - tokenValueUSD?: string; // e.g., "76500000000" (6 decimals) - usdcBorrowed?: string; // Legacy field, prefer 'borrowed' - borrowed?: string; // e.g., "50000000000" (6 decimals) + tokenValueUSD: string; // e.g., "76500000000" (6 decimals) + usdcBorrowed: string; // e.g., "50000000000" (6 decimals) outstandingDebt: string; // e.g., "50041100000" (6 decimals) - healthFactor: number; // e.g., 15300 (basis points) or 2.5 (direct ratio) - healthStatus?: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" + healthFactor: number; // e.g., 15300 (representing 153.00%) + healthStatus: string; // "HEALTHY", "WARNING", "CRITICAL", "LIQUIDATABLE" status: string; // "ACTIVE", "CLOSED", "LIQUIDATED", "SETTLED", "REPAID" - maxBorrowCapacity?: string; // e.g., "53550000000" (6 decimals) + maxBorrowCapacity: string; // e.g., "53550000000" (6 decimals) missedPayments?: number; // Number of missed payments (0-3) isDefaulted?: boolean; // Whether position has been marked as defaulted - createdAt?: string; - - // New fields from backend update - oaidIssued?: boolean; - oaidTokenId?: number; - partnerLoans?: any[]; // Array of partner loans - totalPartnerDebt?: string; + createdAt: string; } /** diff --git a/src/utils/solvency/ltv.util.ts b/src/utils/solvency/ltv.util.ts deleted file mode 100644 index fc5a193..0000000 --- a/src/utils/solvency/ltv.util.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * LTV (Loan-to-Value) Utility Functions - * Calculations for credit limits based on collateral - */ - -import { SOLVENCY_CONFIG } from '@/constants/solvency.constants'; -import type { CollateralPosition } from '@/types/solvency.types'; - -/** - * Calculate credit limit based on collateral value and LTV ratio - * - * @param collateralValueUSD - Collateral value in USD - * @param ltvRatio - LTV ratio (70 for RWA, 60 for Private Assets) - * @returns Credit limit in USD - */ -export const calculateCreditLimit = ( - collateralValueUSD: number, - ltvRatio: number -): number => { - return collateralValueUSD * (ltvRatio / 100); -}; - -/** - * Calculate total credit limit from multiple collateral positions - * - * @param collateralPositions - Array of collateral positions - * @returns Total credit limit in USD - */ -export const calculateTotalCreditLimit = ( - collateralPositions: CollateralPosition[] -): number => { - return collateralPositions.reduce((total, position) => { - const creditLimit = calculateCreditLimit(position.valueUSD, position.ltvRatio); - return total + creditLimit; - }, 0); -}; - -/** - * Get LTV ratio for a token based on whether it's RWA or Private Asset - * - * @param isRWA - Whether the token is an RWA token - * @returns LTV ratio percentage - */ -export const getLTVRatio = (isRWA: boolean): number => { - return isRWA ? SOLVENCY_CONFIG.ltv.RWA : SOLVENCY_CONFIG.ltv.PRIVATE_ASSET; -}; - -/** - * Calculate available credit (credit limit minus current debt) - * - * @param creditLimit - Total credit limit - * @param currentDebt - Current debt amount - * @returns Available credit amount - */ -export const calculateAvailableCredit = ( - creditLimit: number, - currentDebt: number -): number => { - return Math.max(0, creditLimit - currentDebt); -}; - -/** - * Calculate required collateral value to borrow a specific amount - * - * @param borrowAmountUSD - Desired borrow amount in USD - * @param ltvRatio - LTV ratio to use - * @returns Required collateral value in USD - */ -export const calculateRequiredCollateral = ( - borrowAmountUSD: number, - ltvRatio: number -): number => { - return borrowAmountUSD / (ltvRatio / 100); -}; - -/** - * Calculate credit utilization percentage - * - * @param currentDebt - Current debt amount - * @param creditLimit - Total credit limit - * @returns Utilization percentage (0-100) - */ -export const calculateCreditUtilization = ( - currentDebt: number, - creditLimit: number -): number => { - if (creditLimit === 0) { - return 0; - } - - return Math.min(100, (currentDebt / creditLimit) * 100); -}; - -/** - * Format LTV ratio for display - * - * @param ltvRatio - LTV ratio value - * @returns Formatted string with % symbol - */ -export const formatLTVRatio = (ltvRatio: number): string => { - return `${ltvRatio}%`; -}; - -/** - * Check if user has sufficient credit limit to borrow amount - * - * @param availableCredit - Available credit amount - * @param borrowAmount - Requested borrow amount - * @returns true if sufficient, false otherwise - */ -export const hasSufficientCredit = ( - availableCredit: number, - borrowAmount: number -): boolean => { - return availableCredit >= borrowAmount; -}; - -/** - * Calculate collateral value in USD based on token price - * - * @param tokenAmount - Amount of tokens - * @param tokenPriceUSD - Price per token in USD - * @returns Total collateral value in USD - */ -export const calculateCollateralValueUSD = ( - tokenAmount: number, - tokenPriceUSD: number -): number => { - return tokenAmount * tokenPriceUSD; -}; From 3d5c49816ed0043dac053b9b01b47b5751572f93 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 13:26:54 +0530 Subject: [PATCH 21/65] type error fixed --- src/components/portfolio/RepayLoanModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index b725c0f..b263d0b 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -108,7 +108,7 @@ export const RepayLoanModal = ({ const nextInstallmentAmount = useMemo(() => { if (!schedule) return null; - const nextUnpaid = schedule.installments.find( + const nextUnpaid = schedule.installments?.find( i => i.status === 'PENDING' || i.status === 'MISSED' ); From d2b395223a67dfbbb997c1973e02c19c196a883b Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sun, 11 Jan 2026 16:58:56 +0530 Subject: [PATCH 22/65] feat: Update asset filtering and UI enhancements - Added 'TOKENIZED' status to asset filtering in PayoutView.page.tsx. - Removed distribution results section from SettlementView.page.tsx. - Enhanced AssetDetails.page.tsx with improved layout and additional buttons for navigation. - Updated Portfolio.page.tsx modal styles for better visibility and user experience. --- src/pages/admin/payout/PayoutView.page.tsx | 2 +- .../admin/settlements/SettlementView.page.tsx | 32 - .../marketplace/asset/AssetDetails.page.tsx | 762 +++++++++--------- src/pages/portfolio/Portfolio.page.tsx | 8 +- 4 files changed, 388 insertions(+), 416 deletions(-) diff --git a/src/pages/admin/payout/PayoutView.page.tsx b/src/pages/admin/payout/PayoutView.page.tsx index 29e4332..8f91243 100644 --- a/src/pages/admin/payout/PayoutView.page.tsx +++ b/src/pages/admin/payout/PayoutView.page.tsx @@ -68,7 +68,7 @@ const PayoutViewPage = () => { // Filter for LISTED, ENDED, or AUCTION_DECLARED assets const filteredAssets = allAssets.filter((asset: any) => - ['LISTED', 'ENDED', 'AUCTION_DECLARED'].includes(asset.status) + ['LISTED', 'ENDED', 'AUCTION_DECLARED','TOKENIZED'].includes(asset.status) ); console.log('Filtered assets for payout:', filteredAssets); diff --git a/src/pages/admin/settlements/SettlementView.page.tsx b/src/pages/admin/settlements/SettlementView.page.tsx index 860bb35..abdaced 100644 --- a/src/pages/admin/settlements/SettlementView.page.tsx +++ b/src/pages/admin/settlements/SettlementView.page.tsx @@ -919,38 +919,6 @@ const SettlementViewPage = () => {
-
-

- Distribution Results -

-
-
- Total Distributed: -

- {formatUSDC(distributionResults.totalDistributed)} USDC -

-
-
- Token Holders: -

- {distributionResults.holders} -

-
-
- Total Token-Days: -

- {parseFloat(distributionResults.totalTokenDays).toLocaleString()} -

-
-
- Effective Yield: -

- {distributionResults.effectiveYield} -

-
-
-
-
diff --git a/src/pages/marketplace/asset/AssetDetails.page.tsx b/src/pages/marketplace/asset/AssetDetails.page.tsx index fd36610..d2b262a 100644 --- a/src/pages/marketplace/asset/AssetDetails.page.tsx +++ b/src/pages/marketplace/asset/AssetDetails.page.tsx @@ -653,440 +653,444 @@ const AssetDetailsPage = () => {

Invoice {asset.metadata.invoiceNumber}

-
+
+
-

- Status: {asset.status} -

- -
+ +

+ Status: {asset.status} +

- {/* Chart Section */} -
-
-
-

- ${(purchaseHistory?.purchases && purchaseHistory.purchases.length > 0) ? (parseFloat(purchaseHistory.purchases[0].price) / 1e6).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : (asset.tokenParams.pricePerToken ? (parseFloat(asset.tokenParams.pricePerToken) / 1e6).toFixed(2) : 'N/A')} -

-

Token Price (USDC)

-
+
- {/* New Activity Stats */} -
-
-

Total Activity

-

{purchaseHistory?.totalTransactions || 0}

-
-
-

Direct Buys

-

{purchaseHistory?.metadata?.directPurchases || 0}

-
-
-

Leveraged

-

{purchaseHistory?.metadata?.leveragePurchases || 0}

-
-
+ {/* Chart Section */} +
+
+
+

+ ${(purchaseHistory?.purchases && purchaseHistory.purchases.length > 0) ? (parseFloat(purchaseHistory.purchases[0].price) / 1e6).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : (asset.tokenParams.pricePerToken ? (parseFloat(asset.tokenParams.pricePerToken) / 1e6).toFixed(2) : 'N/A')} +

+

Token Price (USDC)

- {isLoadingHistory ? ( -
-

Loading chart data...

+ + {/* New Activity Stats */} +
+
+

Total Activity

+

{purchaseHistory?.totalTransactions || 0}

- ) : historyError ? ( -
-

Error loading chart data: {historyError}

+
+

Direct Buys

+

{purchaseHistory?.metadata?.directPurchases || 0}

- ) : (formattedChartData.length > 0) ? ( -
- - - - - - - - - { - const date = new Date(timestamp); - return date.toLocaleString(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }} - /> - { - // Format large numbers with K, M suffix - if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; - if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; - return tokens.toFixed(0); - }} - label={{ value: 'Tokens Purchased', angle: -90, position: 'insideRight', style: { fill: '#6B7280', fontSize: 12 } }} - /> - { - const tokens = typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : value; - return [ -
-

{tokens} Tokens

- {props.payload.purchaseMethod && ( -

- - Method: - {props.payload.purchaseMethod} - -

- )} -

{props.payload.purchaseCount} transaction(s)

-
, - '' - ]; - }} - labelFormatter={(timestamp) => { - const date = new Date(timestamp); - return date.toLocaleString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }} - /> - -
-
-
- ) : ( -
-

No purchase activity yet.

+
+

Leveraged

+

{purchaseHistory?.metadata?.leveragePurchases || 0}

- )} +
+ {isLoadingHistory ? ( +
+

Loading chart data...

+
+ ) : historyError ? ( +
+

Error loading chart data: {historyError}

+
+ ) : (formattedChartData.length > 0) ? ( +
+ + + + + + + + + { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }} + /> + { + // Format large numbers with K, M suffix + if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; + if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; + return tokens.toFixed(0); + }} + label={{ value: 'Tokens Purchased', angle: -90, position: 'insideRight', style: { fill: '#6B7280', fontSize: 12 } }} + /> + { + const tokens = typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : value; + return [ +
+

{tokens} Tokens

+ {props.payload.purchaseMethod && ( +

+ + Method: + {props.payload.purchaseMethod} + +

+ )} +

{props.payload.purchaseCount} transaction(s)

+
, + '' + ]; + }} + labelFormatter={(timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }} + /> + +
+
+
+ ) : ( +
+

No purchase activity yet.

+
+ )} +
- {/* Invoice Details */} -
-

Invoice Details

-
-
-

Face Value

-

- {asset.metadata.currency} {parseFloat(asset.metadata.faceValue).toLocaleString()} -

-
-
-

Issue Date

-

- {new Date(asset.metadata.issueDate).toLocaleDateString()} -

-
-
-

Due Date

-

- {new Date(asset.metadata.dueDate).toLocaleDateString()} -

-
-
-

Total Supply

-

- {(parseFloat(asset.tokenParams.totalSupply) / 1e18).toLocaleString()} tokens -

-
-
-

Minimum Investment

-

- {(parseFloat(asset.tokenParams.minInvestment) / 1e18).toLocaleString()} tokens -

-
-
-

Sold Tokens

-

- {(parseFloat(asset.listing?.sold || '0') / 1e18).toLocaleString()} tokens -

-
-
-

Buyer

-

{asset.metadata.buyerName}

-
+ {/* Invoice Details */} +
+

Invoice Details

+
+
+

Face Value

+

+ {asset.metadata.currency} {parseFloat(asset.metadata.faceValue).toLocaleString()} +

+
+
+

Issue Date

+

+ {new Date(asset.metadata.issueDate).toLocaleDateString()} +

+
+
+

Due Date

+

+ {new Date(asset.metadata.dueDate).toLocaleDateString()} +

+
+
+

Total Supply

+

+ {(parseFloat(asset.tokenParams.totalSupply) / 1e18).toLocaleString()} tokens +

+
+
+

Minimum Investment

+

+ {(parseFloat(asset.tokenParams.minInvestment) / 1e18).toLocaleString()} tokens +

+
+
+

Sold Tokens

+

+ {(parseFloat(asset.listing?.sold || '0') / 1e18).toLocaleString()} tokens +

+
+
+

Buyer

+

{asset.metadata.buyerName}

+
- {/* Risk & Blockchain Data */} -
-

Risk & Blockchain Data

-
-
-

Risk Tier

-

{asset.metadata.riskTier}

-
-
-

Token Address

-

- {asset.token?.address || 'N/A'} -

-
-
-

Attestation Hash

-

- {asset.attestation?.hash || 'N/A'} -

-
-
-

Registry Block

-

{asset.registry?.blockNumber || 'N/A'}

-
+ {/* Risk & Blockchain Data */} +
+

Risk & Blockchain Data

+
+
+

Risk Tier

+

{asset.metadata.riskTier}

+
+
+

Token Address

+

+ {asset.token?.address || 'N/A'} +

+
+
+

Attestation Hash

+

+ {asset.attestation?.hash || 'N/A'} +

+
+
+

Registry Block

+

{asset.registry?.blockNumber || 'N/A'}

+
- {/* Right Column - Sticky Buy Panel */} -
-
-
- -
-

Buy Tokens

- - USDC - Leverage - -
+ {/* Right Column - Sticky Buy Panel */} +
+
+
+ +
+

Buy Tokens

+ + USDC + Leverage + +
- -
-
- + +
+
+ + { + const inputValue = e.target.value; + setTokensToBuy(inputValue); + }} + min={(() => { + const totalSupply = parseFloat(asset.tokenParams.totalSupply) / 1e18; + const soldTokens = parseFloat(asset.listing?.sold || '0') / 1e18; + const availableTokens = totalSupply - soldTokens; + const minInvestment = parseFloat(asset.tokenParams.minInvestment) / 1e18; + return availableTokens < minInvestment ? availableTokens : minInvestment; + })()} + className=" border-none text-2xl font-medium text-[#111111] p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0" + /> +

+ Available: {(() => { + const totalSupply = parseFloat(asset.tokenParams.totalSupply) / 1e18; + const soldTokens = parseFloat(asset.listing?.sold || '0') / 1e18; + return (totalSupply - soldTokens).toLocaleString(); + })()} tokens +

+
+
+ +
+ USDC +

+ ${estimatedTotalPrice} USDC +

+
+
+
+
+ Your USDC Balance + + {parseFloat(usdcBalance).toFixed(2)} USDC + +
+ +
+ Min Investment + + {(parseFloat(asset.tokenParams.minInvestment) / 1e18).toLocaleString()} tokens + +
+
+ {purchaseStatus && ( +
+ {purchaseStatus} +
+ )} + +
+
+ + +
+
+ +
{ - const inputValue = e.target.value; - setTokensToBuy(inputValue); - }} - min={(() => { - const totalSupply = parseFloat(asset.tokenParams.totalSupply) / 1e18; - const soldTokens = parseFloat(asset.listing?.sold || '0') / 1e18; - const availableTokens = totalSupply - soldTokens; - const minInvestment = parseFloat(asset.tokenParams.minInvestment) / 1e18; - return availableTokens < minInvestment ? availableTokens : minInvestment; - })()} + value={leverageTokenInput} + onChange={handleLeverageTokenChange} className=" border-none text-2xl font-medium text-[#111111] p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0" /> -

- Available: {(() => { - const totalSupply = parseFloat(asset.tokenParams.totalSupply) / 1e18; - const soldTokens = parseFloat(asset.listing?.sold || '0') / 1e18; - return (totalSupply - soldTokens).toLocaleString(); - })()} tokens -

-
- + +
+ +
+
+

Required Collateral

USDC -

- ${estimatedTotalPrice} USDC +

+ {calculatedMethString || '0.00'} mETH

-
-
- Your USDC Balance - - {parseFloat(usdcBalance).toFixed(2)} USDC +
+
+ Buying Power + + {(() => { + if (!calculatedMethAmount) return '$0.00 USDC'; + const bp = (calculatedMethAmount * methPrice) / (1.5 * 1e6); + return `$${bp.toLocaleString(undefined, { maximumFractionDigits: 2 })} USDC`; + })()}
- -
- Min Investment - - {(parseFloat(asset.tokenParams.minInvestment) / 1e18).toLocaleString()} tokens +
+ mETH Price + + ${(methPrice / 1e6).toLocaleString(undefined, { maximumFractionDigits: 2 })}
- {purchaseStatus && ( -
- {purchaseStatus} -
- )} -
- - - -
-
- -
- -
+
+
+ Health Factor + 1.50 (Initial)
- -
-
-

Required Collateral

-
- mETH -

- {calculatedMethString || '0.00'} mETH -

-
-
-
-
- Buying Power - - {(() => { - if (!calculatedMethAmount) return '$0.00 USDC'; - const bp = (calculatedMethAmount * methPrice) / (1.5 * 1e6); - return `$${bp.toLocaleString(undefined, { maximumFractionDigits: 2 })} USDC`; - })()} - -
-
- mETH Price - - ${(methPrice / 1e6).toLocaleString(undefined, { maximumFractionDigits: 2 })} - -
-
+
+ Liquidation Threshold + 1.10
+
-
-
- Health Factor - 1.50 (Initial) -
-
- Liquidation Threshold - 1.10 + {leveragePurchaseStatus && (() => { + const statusLower = leveragePurchaseStatus.toLowerCase(); + const isSuccess = statusLower.includes('success'); + const isError = statusLower.includes('fail') || statusLower.includes('error'); + const tone = isSuccess + ? 'bg-green-50 text-green-800 border border-green-200' + : isError + ? 'bg-red-50 text-red-800 border border-red-200' + : 'bg-blue-50 text-blue-800 border border-blue-200'; + return ( +
+ {leveragePurchaseStatus}
-
+ ); + })()} + + -
- - -
+ +
+ +
+
); }; diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index ebe3c5f..736add3 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -739,9 +739,9 @@ const PortfolioPage = () => { {/* Yield Claim Confirmation Modal - Burn-to-Claim Model */} {showClaimModal && selectedAssetForClaim && ( -
+
{ This will permanently burn your RWA tokens to claim your pro-rata share of settlement USDC.

-
+

Tokens to Burn

@@ -779,7 +779,7 @@ const PortfolioPage = () => {

-
+

⚠️ Warning: This action is irreversible. Your tokens will be burned permanently.

From 5579afbb347bff2973225de056e7e913f5e6321d Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Sun, 11 Jan 2026 17:11:45 +0530 Subject: [PATCH 23/65] feat: add settlement transaction hash to SecondaryOrder and update TradesTable for settlement button --- src/components/portfolio/TradesTable.tsx | 10 ++++++++++ src/pages/secondary-marketplace/TradingEngine.page.tsx | 2 +- src/types/marketplace.types.ts | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/portfolio/TradesTable.tsx b/src/components/portfolio/TradesTable.tsx index 5bb318e..f5ca620 100644 --- a/src/components/portfolio/TradesTable.tsx +++ b/src/components/portfolio/TradesTable.tsx @@ -337,6 +337,16 @@ export const TradesTable = ({ View Tx )} + + {order.stlTxHash && ( + + )}
USDC Borrowed - Health Factor - Status -
- {formatHealthFactor(position.currentHealthFactor)} -
-
@@ -440,22 +472,41 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {/* Actions */} - {!position.isDefaulted ? ( - - ) : ( - - {position.isDefaulted ? 'Defaulted' : 'No Action'} - - )} +
+ {/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} + {!position.isDefaulted && position.oaidCreditIssued && hasDebt && ( + + )} + + {/* Withdraw Button - Show when no loan issued yet and collateral exists */} + {canWithdraw && ( + + )} + + {/* No Action State */} + {!position.oaidCreditIssued && !canWithdraw && !position.isDefaulted && ( + No Actions + )} + + {position.isDefaulted && ( + Defaulted + )} +
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - {!position.isDefaulted && position.oaidCreditIssued && hasDebt && ( + {!position.isDefaulted && ! position.oaidCreditIssued && hasDebt && (
{/* Next Installment Hint */} - {nextInstallmentAmount && ( + {nextInstallmentAmount && interestAmount > 0 && (
- Next installment: {formatUSD(nextInstallmentAmount)} - {schedule && schedule.missedPayments > 0 && ( - (OVERDUE) +
Principal: {formatUSD(totalInstallmentAmount)}
+
Interest: {interestAmount.toFixed(6)}
+
+ Total Debt: {outstandingDebt.toFixed(6)} +
+ {position.missedPayments > 0 && ( +
⚠️ {position.missedPayments} payment(s) overdue
)}
)} - {/* Amount Input */} -
- -
- { - const val = e.target.value.replace(/[^0-9.]/g, ''); - setRepayAmount(val); - }} - placeholder="0.00" - className="pr-16 text-lg" - disabled={isApproving || isRepaying} - /> -
- USDC + {/* Installment Buttons */} + {position.repaymentSchedule && position.repaymentSchedule.length > 0 && ( +
+ +
+ {position.repaymentSchedule.map((installment) => { + const isLastInstallment = installment.installmentNumber === position.numberOfInstallments; + const baseAmount = parseFloat(installment.amount) / 1e6; + const finalAmount = isLastInstallment ? baseAmount + interestAmount : baseAmount; + const isPaid = installment.status === 'PAID'; + const isOverdue = installment.status === 'MISSED'; + const isPending = installment.status === 'PENDING'; + + return ( + + ); + })}
- {!isAmountValid && parseFloat(repayAmount) > 0 && ( -

- {parseFloat(repayAmount) > outstandingDebt - ? `Amount exceeds outstanding debt of ${formatUSD(getOutstandingDebt(position))}` - : 'Please enter a valid amount'} -

- )} -
- - {/* Quick Amount Buttons */} -
- {nextInstallmentAmount && ( - - )} - - -
+ )} {/* Error Display */} {error && ( @@ -380,35 +361,23 @@ export const RepayLoanModal = ({ )} {/* Progress Steps */} - {(isApproving || isRepaying) && ( + {(isApproving || isRepaying) && selectedInstallment && (
- {currentStep === 'approving' && 'Approving USDC...'} - {currentStep === 'repaying' && 'Processing repayment...'} + {currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} + {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} {currentStep === 'syncing' && 'Syncing with backend...'}
)} - {/* Action Button */} - - + {/* Action Button - Removed since we have installment buttons */} {/* Info */}
- After repayment, your loan schedule and available credit will be updated automatically. + Click on any pending installment to make a payment. Interest will be added to the last installment.
)} diff --git a/src/types/solvency.types.ts b/src/types/solvency.types.ts index 9524dc7..e8c3b63 100644 --- a/src/types/solvency.types.ts +++ b/src/types/solvency.types.ts @@ -286,7 +286,12 @@ export interface Position { missedPayments: number; // Number of missed payments (0-3) isDefaulted: boolean; // Whether position has been marked as defaulted oaidCreditIssued: boolean; // Whether OAID credit was issued - repaymentSchedule: any[]; // Repayment schedule array + repaymentSchedule: Array<{ + installmentNumber: number; + dueDate: string; + amount: string; + status: 'PAID' | 'PENDING' | 'MISSED'; + }>; // Repayment schedule array nextPaymentDueDate?: string; // Optional next payment due date depositTxHash: string; // Deposit transaction hash depositBlockNumber: number; // Deposit block number From 362cae435f8edb09162ebf8519e9aae6c3cf8481 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Sun, 11 Jan 2026 23:54:11 +0530 Subject: [PATCH 28/65] refactor: enhance UI components for consistency and improve loading states in MyLoansTable, NoAssetsModal, RepayLoanModal, TradesTable, DepositCollateralModal, and TradingEngine --- src/components/portfolio/MyLoansTable.tsx | 33 +++--- src/components/portfolio/NoAssetsModal.tsx | 54 ++++++---- src/components/portfolio/RepayLoanModal.tsx | 102 ++++++++++-------- src/components/portfolio/TradesTable.tsx | 7 +- .../components/DepositCollateralModal.tsx | 76 +++++++------ .../TradingEngine.page.tsx | 6 +- 6 files changed, 154 insertions(+), 124 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 0c58b0a..9ed9be9 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -22,6 +22,7 @@ import { solvencyService } from '../../lib/api/solvency.service'; import { solvencyContractService } from '../../lib/api/solvency-contract.service'; import { format } from 'date-fns'; import { RepayLoanModal } from './RepayLoanModal'; +import { PageLoader } from '../ui/page-loader'; interface MyLoansTableProps { positions: Position[]; @@ -230,8 +231,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr if (isLoading) { return ( -
-
+
+
); } @@ -258,7 +259,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr return (
{/* Filter Bar */} -
+
Filter: @@ -329,7 +330,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
) : ( - + toggleRowExpansion(position.positionId, e)} > @@ -477,15 +478,15 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {/* Actions */} - @@ -390,6 +442,7 @@ export function LoansView() { {filteredPositions.map((position) => ( + position.borrowedAmountFormatted !== '$0.00' && position.outstandingDebtFormatted !== '$0.00' && ( - + - + ) ))}
@@ -373,7 +374,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
-
+
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} {!position.isDefaulted && ! position.oaidCreditIssued && hasDebt && ( @@ -529,7 +530,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {/* Loan Details Header */}
{/* LTV Ratio */} -
+
LTV Ratio @@ -540,7 +541,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{/* Total Installments */} -
+
Installments @@ -551,7 +552,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{/* Missed Payments */} -
+
Missed @@ -562,7 +563,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{/* Payment Interval */} -
+
Interval @@ -589,7 +590,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr ? 'bg-green-50 border-green-200' : installment.status === 'MISSED' ? 'bg-red-50 border-red-200' - : 'bg-white border-gray-200' + : 'bg-transparent border-gray-200' }`} >
@@ -687,7 +688,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr {/* Withdraw Confirmation Modal */} {selectedPosition && showWithdrawModal && createPortal(
-
+
{/* Header */}
diff --git a/src/components/portfolio/NoAssetsModal.tsx b/src/components/portfolio/NoAssetsModal.tsx index 3c0a4bc..c3fdd1a 100644 --- a/src/components/portfolio/NoAssetsModal.tsx +++ b/src/components/portfolio/NoAssetsModal.tsx @@ -1,7 +1,6 @@ // src/components/portfolio/NoAssetsModal.tsx import { useNavigate } from 'react-router-dom'; import { Lock, Upload, ShoppingCart, Info, X } from 'lucide-react'; -import { Button } from '../ui/button'; interface NoAssetsModalProps { isOpen: boolean; @@ -14,56 +13,69 @@ export const NoAssetsModal = ({ isOpen, onClose }: NoAssetsModalProps) => { if (!isOpen) return null; return ( -
-
- -
-
- +
+
+
+ +
+
-

+

Get Started with Borrowing

-

+

To increase your credit limit, you need RWA tokens to use as collateral. Choose an option below to get started:

{ navigate('/issuers'); onClose(); }} >
-

+

Deposit Private Assets

-

+

Tokenize your real-world assets to use as collateral.

{ navigate('/marketplace'); onClose(); }} >
-

+

Buy from Marketplace

-

+

Purchase tokenized assets from other users.

-
- -
+
+ +

How it works: Once you have RWA tokens in your portfolio, you can deposit them as collateral to increase your credit limit. -

+

diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index a470f78..65b308a 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -12,12 +12,11 @@ */ import { useState, useEffect, useMemo } from 'react'; -import { X, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react'; +import { X, RefreshCw, CheckCircle } from 'lucide-react'; import { ethers } from 'ethers'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; import { solvencyContractService } from '../../lib/api/solvency-contract.service'; -import { Button } from '../ui/button'; interface RepayLoanModalProps { isOpen: boolean; @@ -212,53 +211,62 @@ export const RepayLoanModal = ({ if (!isOpen) return null; return ( -
-
+
+
{/* Header */}
-

Repay Loan

- + +
{success ? ( // Success State
-
- +
+
-

+

Repayment Successful!

-

+

Your loan has been updated. The page will refresh automatically.

) : (
{/* Position Info */} -
+
- Position #{position.positionId} - + Position #{position.positionId} + {getTokenSymbol(position.collateralTokenAddress)}
- Outstanding Debt + Outstanding Debt {isFetchingDebt ? (
- Loading... + Loading...
) : ( - + {actualDebt ? `$${ethers.formatUnits(actualDebt, 6)}` : formatUSD(outstandingDebt)} @@ -269,16 +277,15 @@ export const RepayLoanModal = ({ {/* Next Installment Hint */} {nextInstallmentAmount && interestAmount > 0 && ( -
- -
+
+
Principal: {formatUSD(totalInstallmentAmount)}
Interest: {interestAmount.toFixed(6)}
-
+
Total Debt: {outstandingDebt.toFixed(6)}
{position.missedPayments > 0 && ( -
⚠️ {position.missedPayments} payment(s) overdue
+
⚠️ {position.missedPayments} payment(s) overdue
)}
@@ -287,7 +294,7 @@ export const RepayLoanModal = ({ {/* Installment Buttons */} {position.repaymentSchedule && position.repaymentSchedule.length > 0 && (
-
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - {!position.isDefaulted && !position.oaidCreditIssued && hasDebt && ( + { ( )}
- {/* Withdrawal Details */} -
-
-
- {getTokenSymbol(selectedPosition.collateralTokenAddress)} - - {formatCollateralAmount(selectedPosition.collateralAmount, 18)} tokens - + {/* Content */} +
+
+ {/* Withdrawal Details */} +
+
+ Token + + {getTokenSymbol(selectedPosition.collateralTokenAddress)} + +
+
+ Amount + + {formatCollateralAmount(selectedPosition.collateralAmount, 18)} tokens + +
+
+ Value + + {formatUSD(selectedPosition.tokenValueUSD)} + +
-
- - Collateral Value - - - {formatUSD(selectedPosition.tokenValueUSD)} - -
-
-
-
- - - -

- Your collateral will be returned to your wallet. Please confirm the transaction in your wallet. -

+ {/* Warning */} +
+
+ +

+ Your collateral will be returned to your wallet. Please confirm the transaction in your wallet. +

+
-
-
- {/* Processing State */} - {withdrawingPositionId === selectedPosition.positionId && ( -
-
-

Processing withdrawal...

-

Please confirm in your wallet

+ {/* Processing State */} + {withdrawingPositionId === selectedPosition.positionId && ( +
+
+ +
+

Processing withdrawal...

+

Please confirm in your wallet

+
+
+
+ )}
- )} +
- {/* Action Buttons */} + {/* Footer */} {!withdrawingPositionId && ( -
+
+ diff --git a/src/components/portfolio/PortfolioStats.tsx b/src/components/portfolio/PortfolioStats.tsx index c3aa563..ba6ca1c 100644 --- a/src/components/portfolio/PortfolioStats.tsx +++ b/src/components/portfolio/PortfolioStats.tsx @@ -136,7 +136,6 @@ export const PortfolioStats = ({
- {success ? ( - // Success State -
-
- -
-

- Repayment Successful! -

-

- Your loan has been updated. The page will refresh automatically. -

-
- ) : ( -
- {/* Position Info */} -
-
- Position #{position.positionId} - - {getTokenSymbol(position.collateralTokenAddress)} - + {/* Content */} +
+ {success ? ( + // Success State +
+
+
-
- Outstanding Debt - {isFetchingDebt ? ( -
- - Loading... -
- ) : ( - - {actualDebt - ? `$${ethers.formatUnits(actualDebt, 6)}` - : formatUSD(outstandingDebt)} +

Repayment Successful!

+

Your loan has been updated. Refreshing...

+
+ ) : ( +
+ {/* Position Info */} +
+
+ Position + + #{position.positionId} • {getTokenSymbol(position.collateralTokenAddress)} + +
+
+ Outstanding Debt + {isFetchingDebt ? ( +
+ + Loading... +
+ ) : ( + + {actualDebt + ? `$${ethers.formatUnits(actualDebt, 6)}` + : formatUSD(outstandingDebt)} - )} + )} +
-
- {/* Next Installment Hint */} - {nextInstallmentAmount && interestAmount > 0 && ( -
-
-
Principal: {formatUSD(totalInstallmentAmount)}
-
Interest: {interestAmount.toFixed(6)}
-
- Total Debt: {outstandingDebt.toFixed(6)} + {/* Debt Breakdown */} + {nextInstallmentAmount && interestAmount > 0 && ( +
+
+

Debt Breakdown

+
+
+
+ Principal + {formatUSD(totalInstallmentAmount)} +
+
+ Interest + ${interestAmount.toFixed(6)} +
+
+ Total Debt + ${outstandingDebt.toFixed(6)} +
+ {position.missedPayments > 0 && ( +
+ ⚠️ {position.missedPayments} payment(s) overdue +
+ )}
- {position.missedPayments > 0 && ( -
⚠️ {position.missedPayments} payment(s) overdue
- )}
-
- )} - - {/* Installment Buttons */} - {position.repaymentSchedule && position.repaymentSchedule.length > 0 && ( -
- -
- {position.repaymentSchedule.map((installment) => { - const isLastInstallment = installment.installmentNumber === position.numberOfInstallments; - const baseAmount = parseFloat(installment.amount) / 1e6; - const finalAmount = isLastInstallment ? baseAmount + interestAmount : baseAmount; - const isPaid = installment.status === 'PAID'; - const isOverdue = installment.status === 'MISSED'; - const isPending = installment.status === 'PENDING'; - - return ( - - ); - })} -
-
- )} - - {/* Error Display */} - {error && ( -
-

- ⚠️ - {error} -

-
- )} - - {/* Progress Steps */} - {(isApproving || isRepaying) && selectedInstallment && ( -
-
- -
- {currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} - {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} - {currentStep === 'syncing' && 'Syncing with backend...'} + + ); + })}
-
- )} + )} - {/* Action Button - Removed since we have installment buttons */} - {/* Info */} -
- Click on any pending installment to make a payment. Interest will be added to the last installment. + {/* Error Display */} + {error && ( +
+ {error} +
+ )} + + {/* Progress Steps */} + {(isApproving || isRepaying) && selectedInstallment && ( +
+
+ +
+ {currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} + {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} + {currentStep === 'syncing' && 'Syncing with backend...'} +
+
+
+ )} + + {/* Info */} + {!selectedInstallment && ( +
+

+ Select an installment above to continue +

+
+ )}
+ )} +
+ + {/* Footer */} + {!success && ( +
+ + +
)}
diff --git a/src/pages/admin/loans/LoansView.page.tsx b/src/pages/admin/loans/LoansView.page.tsx index cbcc82c..3c20536 100644 --- a/src/pages/admin/loans/LoansView.page.tsx +++ b/src/pages/admin/loans/LoansView.page.tsx @@ -24,6 +24,21 @@ export function LoansView() { const [processingId, setProcessingId] = useState(null); const [successMessage, setSuccessMessage] = useState(null); + // Confirmation modal state + const [confirmModal, setConfirmModal] = useState<{ + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + isDangerous?: boolean; + }>({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + isDangerous: false, + }); + // Safe formatting helper for USDC values const formatUSDC = (value: string | null | undefined): string => { if (!value || value === '0') return '$0.00'; @@ -100,112 +115,150 @@ export function LoansView() { // Admin Operation: Mark Missed Payment const handleMarkMissedPayment = async (positionId: number) => { - if (!confirm(`Mark payment as missed for position #${positionId}?`)) return; - - setProcessingId(positionId); - setError(null); - setSuccessMessage(null); - - try { - const result = await solvencyService.markMissedPayment(positionId); - setSuccessMessage(`✅ Missed payment marked! TX: ${result.txHash?.slice(0, 10)}...`); - await fetchPositions(); - } catch (err: any) { - setError(err.message || 'Failed to mark missed payment'); - } finally { - setProcessingId(null); - } + setConfirmModal({ + isOpen: true, + title: 'Mark Missed Payment', + message: `Are you sure you want to mark payment as missed for position #${positionId}?`, + isDangerous: false, + onConfirm: async () => { + setConfirmModal({ ...confirmModal, isOpen: false }); + setProcessingId(positionId); + setError(null); + setSuccessMessage(null); + + try { + // Step 1: Execute blockchain transaction + const result = await solvencyService.markMissedPayment(positionId); + + // Step 2: Sync backend with blockchain to update missed payments count + await solvencyService.adminSyncPosition(positionId); + + // Step 3: Refresh UI with updated data + await fetchPositions(); + + setSuccessMessage(`✅ Missed payment marked! TX: ${result.txHash?.slice(0, 10)}...`); + } catch (err: any) { + setError(err.message || 'Failed to mark missed payment'); + } finally { + setProcessingId(null); + } + }, + }); }; // Admin Operation: Mark Defaulted const handleMarkDefaulted = async (positionId: number) => { - if (!confirm(`⚠️ Mark position #${positionId} as DEFAULTED? This action is irreversible.`)) return; - - setProcessingId(positionId); - setError(null); - setSuccessMessage(null); - - try { - const result = await solvencyService.markDefaulted(positionId); - setSuccessMessage(`✅ Position marked as defaulted! TX: ${result.txHash?.slice(0, 10)}...`); - await fetchPositions(); - } catch (err: any) { - setError(err.message || 'Failed to mark as defaulted'); - } finally { - setProcessingId(null); - } + setConfirmModal({ + isOpen: true, + title: 'Mark Position as Defaulted', + message: `Are you sure you want to mark position #${positionId} as DEFAULTED? This action is irreversible.`, + isDangerous: true, + onConfirm: async () => { + setConfirmModal({ ...confirmModal, isOpen: false }); + setProcessingId(positionId); + setError(null); + setSuccessMessage(null); + + try { + // Step 1: Execute blockchain transaction + const result = await solvencyService.markDefaulted(positionId); + + // Step 2: Sync backend with blockchain to update position status + await solvencyService.adminSyncPosition(positionId); + + // Step 3: Refresh UI with updated data + await fetchPositions(); + + setSuccessMessage(`✅ Position marked as defaulted! TX: ${result.txHash?.slice(0, 10)}...`); + } catch (err: any) { + setError(err.message || 'Failed to mark as defaulted'); + } finally { + setProcessingId(null); + } + }, + }); }; // Admin Operation: Liquidate Position const handleLiquidate = async (positionId: number) => { - if (!confirm(`⚠️ Liquidate position #${positionId}? Collateral will be transferred to YieldVault.`)) return; - - setProcessingId(positionId); - setError(null); - setSuccessMessage(null); - - try { - const result = await solvencyService.liquidatePosition(positionId); - setSuccessMessage( - `✅ Position liquidated! TX: ${result.txHash?.slice(0, 10)}... | ` + - `Marketplace ID: ${result.marketplaceAssetId?.slice(0, 10)}...` - ); - await fetchPositions(); - } catch (err: any) { - setError(err.message || 'Failed to liquidate position'); - } finally { - setProcessingId(null); - } + setConfirmModal({ + isOpen: true, + title: 'Liquidate Position', + message: `Are you sure you want to liquidate position #${positionId}? Collateral will be transferred to YieldVault.`, + isDangerous: true, + onConfirm: async () => { + setConfirmModal({ ...confirmModal, isOpen: false }); + setProcessingId(positionId); + setError(null); + setSuccessMessage(null); + + try { + // Step 1: Execute blockchain transaction + const result = await solvencyService.liquidatePosition(positionId); + + // Step 2: Sync backend with blockchain to update position status + await solvencyService.adminSyncPosition(positionId); + + // Step 3: Refresh UI with updated data + await fetchPositions(); + + setSuccessMessage( + `✅ Position liquidated! TX: ${result.txHash?.slice(0, 10)}... | ` + + `Marketplace ID: ${result.marketplaceAssetId?.slice(0, 10)}...` + ); + } catch (err: any) { + setError(err.message || 'Failed to liquidate position'); + } finally { + setProcessingId(null); + } + }, + }); }; // Admin Operation: Settle Liquidation const handleSettleLiquidation = async (positionId: number) => { - if (!confirm(`Settle liquidation for position #${positionId}? This will burn tokens and distribute yield.`)) return; - - setProcessingId(positionId); - setError(null); - setSuccessMessage(null); - - try { - const result = await solvencyService.settleLiquidation(positionId); - - const yieldReceived = result.yieldReceived - ? parseFloat(ethers.formatUnits(result.yieldReceived, 6)) - : 0; - const debtRepaid = result.debtRepaid - ? parseFloat(ethers.formatUnits(result.debtRepaid, 6)) - : 0; - const userRefund = result.userRefund - ? parseFloat(ethers.formatUnits(result.userRefund, 6)) - : 0; - - setSuccessMessage( - `✅ Liquidation settled! TX: ${result.txHash?.slice(0, 10)}... | ` + - `Yield: $${yieldReceived.toFixed(2)} | Debt: $${debtRepaid.toFixed(2)} | Refund: $${userRefund.toFixed(2)}` - ); - await fetchPositions(); - } catch (err: any) { - setError(err.message || 'Failed to settle liquidation'); - } finally { - setProcessingId(null); - } - }; - - // Admin Operation: Manual Sync Position - const handleSyncPosition = async (positionId: number) => { - setProcessingId(positionId); - setError(null); - setSuccessMessage(null); - - try { - await solvencyService.adminSyncPosition(positionId); - setSuccessMessage(`✅ Position #${positionId} synced successfully!`); - await fetchPositions(); - } catch (err: any) { - setError(err.message || 'Failed to sync position'); - } finally { - setProcessingId(null); - } + setConfirmModal({ + isOpen: true, + title: 'Settle Liquidation', + message: `Are you sure you want to settle liquidation for position #${positionId}? This will burn tokens and distribute yield.`, + isDangerous: false, + onConfirm: async () => { + setConfirmModal({ ...confirmModal, isOpen: false }); + setProcessingId(positionId); + setError(null); + setSuccessMessage(null); + + try { + // Step 1: Execute blockchain transaction + const result = await solvencyService.settleLiquidation(positionId); + + // Step 2: Sync backend with blockchain to update position status + await solvencyService.adminSyncPosition(positionId); + + // Step 3: Refresh UI with updated data + await fetchPositions(); + + const yieldReceived = result.yieldReceived + ? parseFloat(ethers.formatUnits(result.yieldReceived, 6)) + : 0; + const debtRepaid = result.debtRepaid + ? parseFloat(ethers.formatUnits(result.debtRepaid, 6)) + : 0; + const userRefund = result.userRefund + ? parseFloat(ethers.formatUnits(result.userRefund, 6)) + : 0; + + setSuccessMessage( + `✅ Liquidation settled! TX: ${result.txHash?.slice(0, 10)}... | ` + + `Yield: $${yieldReceived.toFixed(2)} | Debt: $${debtRepaid.toFixed(2)} | Refund: $${userRefund.toFixed(2)}` + ); + } catch (err: any) { + setError(err.message || 'Failed to settle liquidation'); + } finally { + setProcessingId(null); + } + }, + }); }; // UI Helper: Status Badge @@ -382,7 +435,6 @@ export function LoansView() {
Collateral Borrowed OutstandingHealth Factor Missed Payments Status Actions
#{position.positionId} @@ -409,27 +462,12 @@ export function LoansView() { {position.outstandingDebtFormatted} -
- - {position.healthFactorFormatted} - - {getHealthStatusBadge(position.healthStatus)} -
-
0 ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-600' }`}> - {position.missedPayments} / 3 + {Math.min(position.missedPayments, 3)} / 3 @@ -464,7 +502,7 @@ export function LoansView() { )} {/* Liquidate - Available for liquidatable positions */} - {position.healthStatus === 'LIQUIDATABLE' && position.status === 'ACTIVE' && ( + {( +
@@ -509,6 +539,49 @@ export function LoansView() { )}
+ + {/* Confirmation Modal */} + {confirmModal.isOpen && ( +
+
+ {/* Header */} +
+

+ {confirmModal.title} +

+
+ + {/* Body */} +
+

+ {confirmModal.message} +

+
+ + {/* Footer */} +
+ + +
+
+
+ )}
); } diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 0227ce7..d914a11 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -382,10 +382,12 @@ export const DepositCollateralModal = ({ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#111111] focus:border-transparent" > - {portfolio.filter(asset => (asset.purchaseType === 'STATIC' && !asset?.yieldInfo?.settlementDistributed)).map((asset) => ( - + {portfolio.map((asset) => ( + asset.status !== 'CLAIMED' && ( + + ) ))}
diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index 5225b01..bc30c30 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -174,6 +174,12 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: B if (requestedDuration > maxDuration) { const maxInstallments = Math.floor(maxDuration / (86400)); + if (maxInstallments === 0) { + return { + loanDuration: 0, + installmentError: `This asset has a very short repayment time, so a loan cannot be taken with this position.` + }; + } return { loanDuration: 0, installmentError: `This asset only allows up to ${maxInstallments} installments.` @@ -259,13 +265,7 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData , onClose}: B {/* Main Borrow Interface - Clean Swap Style */}
{/* Close Button */} - + {/* Collateral Section (Top - Like "Sell") */}
diff --git a/src/pages/marketplace/Marketplace.page.tsx b/src/pages/marketplace/Marketplace.page.tsx index 3af160b..0c78b8c 100644 --- a/src/pages/marketplace/Marketplace.page.tsx +++ b/src/pages/marketplace/Marketplace.page.tsx @@ -188,7 +188,8 @@ const MarketplacePage = () => { // Target amount is face value // @ts-ignore - const targetAmount = parseFloat(listing.metadata?.faceValue || listing.faceValue || '0'); + const targetAmount = totalSupply * pricePerToken; + // const targetAmount = parseFloat(listing.metadata?.faceValue || listing.faceValue || '0'); return { id: listing.assetId, diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 4418c23..7d431d6 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -519,7 +519,6 @@ const PortfolioPage = () => { @@ -591,7 +590,6 @@ const PortfolioPage = () => {
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - { ( + {!position.isDefaulted && position.oaidCreditIssued && hasDebt && (
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - {!position.isDefaulted && position.oaidCreditIssued && hasDebt && ( + {!position.isDefaulted && !position.oaidCreditIssued && !hasDebt && (
diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index c0be0f9..295efbf 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -17,6 +17,7 @@ import { ethers } from 'ethers'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; import { solvencyContractService } from '../../lib/api/solvency-contract.service'; +import { PageLoader } from '../ui/page-loader'; interface RepayLoanModalProps { isOpen: boolean; @@ -279,7 +280,8 @@ export const RepayLoanModal = ({ Outstanding Debt {isFetchingDebt ? (
- + <> + Loading...
) : ( @@ -403,7 +405,9 @@ export const RepayLoanModal = ({ {(isApproving || isRepaying) && selectedInstallment && (
- +
+ +
{currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 17ca60b..f3d3bf4 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -10,6 +10,7 @@ import { useState, useEffect, useMemo } from 'react'; import { useAccount } from 'wagmi'; import { ethers } from 'ethers'; import { useCallback } from 'react'; +import { X, CheckCircle, RefreshCw, AlertCircle } from 'lucide-react'; import { portfolioService, type PortfolioAsset } from '../../../lib/api/portfolio.service'; import { assetService } from '../../../lib/api/asset.service'; import { solvencyContractService } from '../../../lib/api/solvency-contract.service'; @@ -311,42 +312,33 @@ export const DepositCollateralModal = ({ if (!isOpen) return null; return ( -
-
- {/* Header */} -
-
-

- {initialAsset ? 'Add More Collateral' : 'Deposit Collateral'} -

-

- {initialAsset - ? `Add more ${initialAsset?.metadata.assetName} to your position.` - : 'Deposit RWA tokens to create a credit line'} -

-
+
+
+ {/* Header */} +
+
+

+ {initialAsset ? 'Add More Collateral' : 'Deposit Collateral'} +

+

+ {initialAsset + ? `Add more ${initialAsset?.metadata.assetName} to your position` + : 'Deposit RWA tokens to create a credit line'} +

+
{!isProcessing && ( )}
+ {/* Content */} +
+ {/* Loading State */} {isLoading && (
@@ -356,254 +348,244 @@ export const DepositCollateralModal = ({
)} - {/* Error State */} - {error && !isLoading && ( -
-

- ⚠️ - {error} -

-
- )} - - {/* Select Asset Step */} - {step === 'select' && !isLoading && (portfolio.length > 0 || initialAsset) && ( -
- {/* Asset Selection */} - {!initialAsset && ( -
- - -
- )} - - {/* Asset Details */} - {selectedAsset && assetDetails && ( - <> - {/* Token Balance */} -
-
- Your Balance - - {parseFloat(tokenBalance).toFixed(2)} tokens - -
-
- Token Price - - ${(parseFloat(assetDetails.listing.price) / 1e6).toFixed(6)} per token - -
-
+ {/* Error State */} + {error && !isLoading && ( +
+ {error} +
+ )} - {/* Deposit Amount */} + {/* Select Asset Step */} + {step === 'select' && !isLoading && (portfolio.length > 0 || initialAsset) && ( +
+ {/* Asset Selection */} + {!initialAsset && (
-
+ )} + + {/* Asset Details */} + {selectedAsset && assetDetails && ( + <> + {/* Token Balance */} +
+
+ Your Balance + + {parseFloat(tokenBalance).toFixed(2)} tokens + +
+
+ Token Price + + ${(parseFloat(assetDetails.listing.price) / 1e6).toFixed(6)} per token + +
+
- {/* Collateral Preview */} - {depositAmount && parseFloat(depositAmount) > 0 && !depositError && ( -
-

- Credit Line Preview -

-
-
- Collateral Value - - ${parseFloat(collateralValueUSD).toFixed(2)} - -
+ {/* Deposit Amount */} +
+ +
+ setDepositAmount(e.target.value)} + placeholder="0.00" + className="w-full bg-slate-50 border-none rounded-2xl px-4 py-6 pr-20 font-geist text-sm text-slate-900 transition-all focus:ring-2 focus:ring-slate-900/5 focus:bg-white outline-none placeholder:text-slate-400 hover:bg-slate-100" + min="0" + max={tokenBalance} + step="0.01" + /> + +
+ {depositError && ( +

{depositError}

+ )} +
+ + {/* Collateral Preview */} + {depositAmount && parseFloat(depositAmount) > 0 && !depositError && ( +
- LTV Ratio - 70% +

Credit Line Preview

-
-
- Estimated Credit Line - +
+
+ Collateral Value + + ${parseFloat(collateralValueUSD).toFixed(2)} + +
+
+ LTV Ratio + 70% +
+
+ Estimated Credit Line + ${parseFloat(estimatedCreditLine).toFixed(2)}
-
-

- You'll be able to borrow up to ${parseFloat(estimatedCreditLine).toFixed(2)} USDC -

-
- )} - - {/* Important Notice */} -
-
- - - -
-

- Important Information +

+ You'll be able to borrow up to ${parseFloat(estimatedCreditLine).toFixed(2)} USDC

-
    -
  • • Your tokens will be locked as collateral
  • -
  • • Maintain health factor above 110% to avoid liquidation
  • -
  • • You can withdraw after repaying your loan
  • -
-
-
+ )} - {/* Action Buttons */} -
- - -
- - )} -
- )} + {/* Important Notice */} +
+
+ +
+

+ Important Information +

+
    +
  • • Your tokens will be locked as collateral
  • +
  • • Maintain health factor above 110% to avoid liquidation
  • +
  • • You can withdraw after repaying your loan
  • +
+
+
+
- {/* Approval Progress */} - {step === 'approve' && ( -
-
- + + )}
-

- Step 1 of 3: Approving Tokens -

-

- Please confirm the approval transaction in your wallet -

- {txHash && ( - - View transaction → - - )} -
- )} + )} - {/* Deposit Progress */} - {step === 'deposit' && ( -
-
- + {/* Approval Progress */} + {step === 'approve' && ( +
+
+
+ +<>
+

Step 1 of 3: Approving Tokens

+

Please confirm the approval transaction in your wallet

+
+
+
+ {txHash && ( + + View transaction → + + )}
-

- Step 2 of 3: Depositing Collateral -

-

- Please confirm the deposit transaction in your wallet -

-

- This may take up to 5 minutes... -

- {txHash && ( - - View transaction → - - )} -
- )} + )} - {/* Syncing Progress */} - {step === 'syncing' && ( -
-
- + {/* Deposit Progress */} + {step === 'deposit' && ( +
+
+
+ <> +
+

Step 2 of 3: Depositing Collateral

+

Please confirm the deposit transaction in your wallet

+

This may take up to 5 minutes...

+
+
+
+ {txHash && ( + + View transaction → + + )}
-

- Step 3 of 3: Syncing with Platform -

-

- Updating your credit line... -

-
- )} + )} - {/* Success State */} - {step === 'success' && ( -
-
- - - + {/* Syncing Progress */} + {step === 'syncing' && ( +
+
+
+ <> +
+

Step 3 of 3: Syncing with Platform

+

Updating your credit line...

+
+
+
-

- Deposit Successful! -

-

- Your collateral has been deposited and credit line created -

-
-
-

Credit Line Created

-

- ${parseFloat(estimatedCreditLine).toFixed(2)} -

+ )} + + {/* Success State */} + {step === 'success' && ( +
+
+ +
+

Deposit Successful!

+

Your collateral has been deposited and credit line created

+
+
+

Credit Line Created

+

+ ${parseFloat(estimatedCreditLine).toFixed(2)} +

+
+ )} +
+ + {/* Footer */} + {step === 'select' && selectedAsset && assetDetails && ( +
+ + +
)}
diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 23d00c2..29a1eef 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -30,6 +30,7 @@ import { useCreditData } from '../borrow/hooks/useCreditData'; import { DepositCollateralModal } from '../borrow/components/DepositCollateralModal'; import { NoAssetsModal } from '../../components/portfolio/NoAssetsModal'; import { Wavy } from '../../components/ui/wavy'; +import HeroBackground from '../landing/HeroBackground'; const PortfolioPage = () => { @@ -475,7 +476,7 @@ const PortfolioPage = () => { */} {/* */} - + From b6894be2b539a908b96af4c354cf3e0601c98425 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Tue, 13 Jan 2026 22:03:02 +0530 Subject: [PATCH 42/65] fix: Correct loan repayment button visibility logic and adjust input step values for auction bids --- src/components/portfolio/MyLoansTable.tsx | 2 +- .../auction/AuctionDetails.page.tsx | 39 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 9171092..fd0fc93 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -505,7 +505,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - {!position.isDefaulted && !position.oaidCreditIssued && !hasDebt && ( + {!position.isDefaulted && position.oaidCreditIssued && !hasDebt && ( -
+
- Coming Soon + Coming Soon
@@ -396,7 +396,7 @@ const AuctionDetailsPage = () => { value={bidAmount} min={asset.tokenParams?.minInvestment ? (Number(asset.tokenParams.minInvestment) / 1e18).toString() : '0'} max={asset.tokenParams?.totalSupply ? (Number(asset.tokenParams.totalSupply) / 1e18).toString() : '0'} - step="0.01" + step="0.1" onChange={(e) => { const value = e.target.value; if (value === '') { @@ -422,11 +422,9 @@ const AuctionDetailsPage = () => { const effectiveMin = Math.min(minBid, available); if (numValue < effectiveMin) { - setBidAmount(effectiveMin.toFixed(2)); + setBidAmount(effectiveMin.toString()); } else if (numValue > available) { - setBidAmount(available.toFixed(2)); - } else { - setBidAmount(numValue.toFixed(2)); + setBidAmount(available.toString()); } } }} @@ -442,8 +440,8 @@ const AuctionDetailsPage = () => { {/* Price Per Token Input */}
- USDC @@ -458,7 +456,6 @@ const AuctionDetailsPage = () => { disabled={isAuctionAnnounced || !!(asset.listing?.scheduledEndTime && new Date(asset.listing.scheduledEndTime).getTime() <= new Date().getTime())} min={asset.listing?.priceRange?.min ? (Number(asset.listing.priceRange.min) / 1e6).toString() : '0'} max={asset.listing?.priceRange?.max ? (Number(asset.listing.priceRange.max) / 1e6).toString() : '0'} - step="0.01" onChange={(e) => { const value = e.target.value; if (value === '') { @@ -483,18 +480,16 @@ const AuctionDetailsPage = () => { const maxPrice = asset.listing?.priceRange?.max ? Number(asset.listing.priceRange.max) / 1e6 : Infinity; if (numValue < minPrice) { - setPricePerToken(minPrice.toFixed(2)); + setPricePerToken(minPrice.toString()); } else if (numValue > maxPrice) { - setPricePerToken(maxPrice.toFixed(2)); - } else { - setPricePerToken(numValue.toFixed(2)); + setPricePerToken(maxPrice.toString()); } } }} className="w-full border-none text-2xl font-medium text-[#111111] p-0 h-auto bg-transparent focus:outline-none focus:ring-0" />

- Range: ${asset.listing?.priceRange?.min ? (Number(asset.listing.priceRange.min) / 1e6).toFixed(2) : '0.00'} - ${asset.listing?.priceRange?.max ? (Number(asset.listing.priceRange.max) / 1e6).toFixed(2) : '0.00'} + Range: ${asset.listing?.priceRange?.min ? (Number(asset.listing.priceRange.min) / 1e6).toFixed(4) : '0.0000'} - ${asset.listing?.priceRange?.max ? (Number(asset.listing.priceRange.max) / 1e6).toFixed(3) : '0.000'}

From 4d58c5ba282980d1bdae540bac8faa49f6cc9245 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Tue, 13 Jan 2026 22:37:53 +0530 Subject: [PATCH 43/65] style: Improve UI elements in DepositCollateralModal and PortfolioPage with enhanced backgrounds and shadows --- .../components/DepositCollateralModal.tsx | 8 ++--- src/pages/portfolio/Portfolio.page.tsx | 31 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index f3d3bf4..f05c856 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -312,8 +312,8 @@ export const DepositCollateralModal = ({ if (!isOpen) return null; return ( -
-
+
+
{/* Header */}
@@ -360,7 +360,7 @@ export const DepositCollateralModal = ({
{/* Asset Selection */} {!initialAsset && ( -
+
@@ -374,7 +374,7 @@ export const DepositCollateralModal = ({ > {portfolio.map((asset) => ( - asset.status !== 'CLAIMED' && ( + asset.status !== 'CLAIMED' && asset.purchaseType !== 'LEVERAGE' && ( diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 29a1eef..b66c4c2 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -748,21 +748,14 @@ const PortfolioPage = () => { /> {/* Yield Claim Confirmation Modal - Burn-to-Claim Model */} - {showClaimModal && selectedAssetForClaim && ( -
+ { + true && ( +
-
+
🔥
@@ -774,22 +767,24 @@ const PortfolioPage = () => { This will permanently burn your RWA tokens to claim your pro-rata share of settlement USDC.

-
+

Tokens to Burn

- {(parseFloat(selectedAssetForClaim.investorBalance) / 1e18).toFixed(2)} {selectedAssetForClaim.tokenSymbol} + {/* {(parseFloat(selectedAssetForClaim.investorBalance) / 1e18).toFixed(2)} {selectedAssetForClaim.tokenSymbol} */} + 1000

Expected USDC

- ${parseFloat(selectedAssetForClaim.expectedUsdc).toFixed(2)} + {/* ${parseFloat(selectedAssetForClaim.expectedUsdc).toFixed(2)} */} + 10000

-
+

⚠️ Warning: This action is irreversible. Your tokens will be burned permanently.

@@ -801,13 +796,13 @@ const PortfolioPage = () => { setShowClaimModal(false); setSelectedAssetForClaim(null); }} - className="flex-1 px-6 py-3 bg-gray-100 hover:bg-gray-200 text-foreground rounded-xl font-inter font-medium transition-all" + className="flex-1 px-6 py-3 bg-gray-100 hover:bg-gray-200/50 border border-gray-200 text-foreground rounded-xl shadow-lg font-inter font-medium transition-all hover:scale-105" > Cancel From de5fd79649c6f34ff55db3370ab154d89bfd2a0a Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Tue, 13 Jan 2026 22:39:09 +0530 Subject: [PATCH 44/65] fix: Update yield claim confirmation modal condition to use showClaimModal and selectedAssetForClaim --- src/pages/portfolio/Portfolio.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index b66c4c2..e8f3823 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -749,7 +749,7 @@ const PortfolioPage = () => { {/* Yield Claim Confirmation Modal - Burn-to-Claim Model */} { - true && ( + showClaimModal && selectedAssetForClaim && (
Date: Tue, 13 Jan 2026 23:01:35 +0530 Subject: [PATCH 45/65] cleaned the depoist modal --- src/components/portfolio/MyLoansTable.tsx | 2 +- .../components/DepositCollateralModal.tsx | 250 +++++++++--------- 2 files changed, 126 insertions(+), 126 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index fd0fc93..de57660 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -505,7 +505,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{/* Repay Button - Only show when loan was issued (oaidCreditIssued = true) */} - {!position.isDefaulted && position.oaidCreditIssued && !hasDebt && ( + {!position.isDefaulted && position.oaidCreditIssued && hasDebt && ( )}
{/* Content */} -
+
{/* Loading State */} {isLoading && ( @@ -350,37 +351,68 @@ export const DepositCollateralModal = ({ {/* Error State */} {error && !isLoading && ( -
+
{error}
)} {/* Select Asset Step */} {step === 'select' && !isLoading && (portfolio.length > 0 || initialAsset) && ( -
- {/* Asset Selection */} +
+ {/* Asset Selection - Table View */} {!initialAsset && ( -
-
@@ -416,6 +415,37 @@ export const DepositCollateralModal = ({
)} + {/* Selected Asset Display (show after selection) */} + {!initialAsset && selectedAsset && ( +
+
+
+
+ 🏛️ +
+
+

+ {selectedAsset.metadata.assetName} +

+

+ {parseFloat(ethers.formatUnits(selectedAsset.totalAmount, 18)).toFixed(2)} tokens available +

+
+
+ +
+
+ )} + {/* Asset Details */} {selectedAsset && assetDetails && ( <> From b8038a5ede7a170d7b18def4c3dde746bd355263 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 01:05:59 +0530 Subject: [PATCH 48/65] cleaned the modals & fethcing the pending intsllemnt for reapyment only --- src/components/portfolio/MyLoansTable.tsx | 28 ++-- src/components/portfolio/RepayLoanModal.tsx | 151 +++++++++--------- .../components/DepositCollateralModal.tsx | 21 +-- 3 files changed, 100 insertions(+), 100 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 782adbd..157b8c6 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -708,11 +708,24 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr document.body )} - + {/* Withdraw Confirmation Modal */} {selectedPosition && showWithdrawModal && createPortal(
-
+
+ {/* Close Button */} + {!withdrawingPositionId && ( + + )} + {/* Header */}
@@ -720,17 +733,6 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr

Withdraw Collateral

Confirm withdrawal from Position #{selectedPosition.positionId}

- {!withdrawingPositionId && ( - - )}
{/* Content */} diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index 2234f97..bace5cd 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -239,7 +239,16 @@ export const RepayLoanModal = ({ return (
-
+
+ {/* Close Button */} + + {/* Header */}
@@ -247,13 +256,6 @@ export const RepayLoanModal = ({

Repay Loan

Select an installment and confirm payment

-
{/* Content */} @@ -318,77 +320,72 @@ export const RepayLoanModal = ({
)} - {/* Installment Selection */} - {position.repaymentSchedule && position.repaymentSchedule.length > 0 && ( -
- -
- {position.repaymentSchedule.map((installment) => { - const isLastInstallment = installment.installmentNumber === position.numberOfInstallments; - const baseAmount = parseFloat(installment.amount) / 1e6; - const finalAmount = isLastInstallment ? baseAmount + interestAmount : baseAmount; - const isPaid = installment.status === 'PAID'; - const isOverdue = installment.status === 'MISSED'; - const isPending = installment.status === 'PENDING'; - const isSelected = selectedInstallment === installment.installmentNumber; - - return ( - - ); - })} +
+
+
+ {formatUSD(finalAmount)} +
+ {isLastInstallment && interestAmount > 0 && ( +
+ +${interestAmount.toFixed(6)} interest +
+ )} +
+ {isSelected && ( +
+ +
+ )} +
+
-
- )} + ); + })()} {/* Error Display */} {error && ( @@ -415,7 +412,7 @@ export const RepayLoanModal = ({ {!selectedInstallment && (

- Select an installment above to continue + Click the installment above to proceed

)} diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 53950c8..937681d 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -313,7 +313,17 @@ export const DepositCollateralModal = ({ return (
-
+
+ {/* Close Button */} + {!isProcessing && step === 'select' && ( + + )} + {/* Header */}
@@ -327,15 +337,6 @@ export const DepositCollateralModal = ({ ? `Add more ${initialAsset?.metadata.assetName} to your position` : 'Deposit RWA tokens to create a credit line'}

- {!isProcessing && step === 'select' && ( - - )}
{/* Content */} From 206e9889558787c7c656006a2b923fe900778d7b Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 01:23:58 +0530 Subject: [PATCH 49/65] updated modals --- src/components/portfolio/MyLoansTable.tsx | 4 ++-- src/components/portfolio/NoAssetsModal.tsx | 19 +++---------------- src/pages/borrow/BorrowPage.tsx | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 157b8c6..cabfaf7 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -510,8 +510,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{!isLeverage && - hoveredRow === rowKey && parseFloat(asset.yieldInfo?.claimableYield || '0') > 0 && asset.status !== 'CLAIMED' && (
Status + Health Status + Next Payment
+ + {position.status} + +
-
- {isLoadingSchedule ? ( +
+ {isLoadingSchedule && !displaySchedule ? (
- ) : schedule ? ( + ) : scheduleInfo ? (
- {/* Loan Details Header */} -
- {/* LTV Ratio */} -
-
- - LTV Ratio -
-
- {position.initialLTV ? (position.initialLTV / 100).toFixed(0) : 'N/A'}% -
-
+
+ {/* Left Column - 40% - Current Metrics */} +
+

+ Loan Overview +

+
+ {/* LTV Ratio */} +
+
+ + LTV Ratio +
+
+ {position.initialLTV ? (position.initialLTV / 100).toFixed(0) : 'N/A'}% +
+
- {/* Total Installments */} -
-
- - Installments -
-
- {schedule.installmentsPaid} / {schedule.numberOfInstallments} -
-
+ {/* Total Installments */} +
+
+ + Installments +
+
+ {scheduleInfo.installmentsPaid} / {scheduleInfo.numberOfInstallments} +
+
- {/* Missed Payments */} -
-
- - Missed -
-
- {schedule.missedPayments} + {/* Missed Payments */} +
+
+ + Missed +
+
+ {scheduleInfo.missedPayments} +
+
+ + {/* Payment Interval */} +
+
+ + Interval +
+
+ {Math.floor(scheduleInfo.installmentInterval / 86400)}d +
+
- {/* Payment Interval */} -
-
- - Interval -
-
- {Math.floor(schedule.installmentInterval / 86400)}d + {/* Right Column - 60% - New Details */} +
+

+ Position Details +

+
+ + + + + +
{/* Repayment Schedule */}
-

+

Repayment Schedule

- {schedule.installments?.map((installment) => ( + {displaySchedule?.map((installment: any) => (
@@ -622,7 +682,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr #{installment.installmentNumber}
- Due: {format(new Date(installment.dueDate * 1000), 'MMM d, yyyy')} + Due: {format(new Date(installment.dueDate), 'MMM d, yyyy')}
@@ -631,10 +691,10 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
{installment.status} @@ -644,12 +704,12 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr ))}
- {schedule.missedPayments > 0 && ( + {scheduleInfo.missedPayments > 0 && (
- Warning: You have {schedule.missedPayments} missed payment - {schedule.missedPayments > 1 ? 's' : ''}. Please repay immediately to avoid liquidation. + Warning: You have {scheduleInfo.missedPayments} missed payment + {scheduleInfo.missedPayments > 1 ? 's' : ''}. Please repay immediately to avoid liquidation.
)} @@ -808,3 +868,11 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr ); }; + +// Helper component for detail items +const DetailItem = ({ label, value }: { label: string; value: string | number }) => ( +
+ {label} + {value} +
+); diff --git a/src/components/portfolio/RepayLoanModal.tsx b/src/components/portfolio/RepayLoanModal.tsx index bace5cd..268ebf7 100644 --- a/src/components/portfolio/RepayLoanModal.tsx +++ b/src/components/portfolio/RepayLoanModal.tsx @@ -12,7 +12,7 @@ */ import { useState, useEffect, useMemo } from 'react'; -import { X } from 'lucide-react'; +import { X, CheckCircle } from 'lucide-react'; import { ethers } from 'ethers'; import type { Position } from '../../types/solvency.types'; import { solvencyService } from '../../lib/api/solvency.service'; @@ -64,11 +64,11 @@ export const RepayLoanModal = ({ if (!position.repaymentSchedule || position.repaymentSchedule.length === 0 || !actualDebt) { return { totalInstallmentAmount: 0, interestAmount: 0 }; } - + const total = position.repaymentSchedule.reduce((sum, inst) => sum + parseFloat(inst.amount), 0) / 1e6; const actualDebtUSD = parseFloat(ethers.formatUnits(actualDebt, 6)); const interest = actualDebtUSD - total; - + return { totalInstallmentAmount: total, interestAmount: Math.max(0, interest) }; }, [position.repaymentSchedule, actualDebt]); @@ -238,172 +238,192 @@ export const RepayLoanModal = ({ if (!isOpen) return null; return ( -
-
- {/* Close Button */} - - +
+
{/* Header */} -
-
- 💳 +
+
+

Repay Loan

+

Select an installment and confirm payment

-

Repay Loan

-

Select an installment and confirm payment

+
{/* Content */} -
+
{success ? ( // Success State -
-
- +
+
+
-

Repayment Successful!

-

Your loan has been updated. Refreshing...

+

Repayment Successful!

+

Your loan has been updated. Refreshing...

) : ( -
+
{/* Position Info */} -
-
-

Position

-

+

+
+ Position + #{position.positionId} • {getTokenSymbol(position.collateralTokenAddress)} -

+
-
-

Outstanding Debt

+
+ {isFetchingDebt ? ( -
- +
+
) : ( -

- {actualDebt - ? `$${ethers.formatUnits(actualDebt, 6)}` - : formatUSD(outstandingDebt)} -

+
+ Outstanding Debt + + {actualDebt + ? `$${ethers.formatUnits(actualDebt!, 6)}` + : formatUSD(outstandingDebt)} + +
)}
{/* Debt Breakdown */} {nextInstallmentAmount && interestAmount > 0 && ( -
-
-

Principal

-

{formatUSD(totalInstallmentAmount)}

-
-
-

Interest

-

${interestAmount.toFixed(6)}

-
-
-

Total Debt

-

${outstandingDebt.toFixed(6)}

+
+
+

Debt Breakdown

- {position.missedPayments > 0 && ( -
-

- ⚠️ {position.missedPayments} payment(s) overdue -

+
+
+ Principal + {formatUSD(totalInstallmentAmount)}
- )} +
+ Interest + ${interestAmount.toFixed(6)} +
+
+ Total Debt + ${outstandingDebt.toFixed(6)} +
+ {position.missedPayments > 0 && ( +
+ ⚠️ {position.missedPayments} payment(s) overdue +
+ )} +
)} - {/* Next Installment Payment */} - {position.repaymentSchedule && position.repaymentSchedule.length > 0 && (() => { - // Find the next unpaid installment (PENDING or MISSED) - const nextInstallment = position.repaymentSchedule.find( - i => i.status === 'PENDING' || i.status === 'MISSED' - ); - - if (!nextInstallment) return null; - - const isLastInstallment = nextInstallment.installmentNumber === position.numberOfInstallments; - const baseAmount = parseFloat(nextInstallment.amount) / 1e6; - const finalAmount = isLastInstallment ? baseAmount + interestAmount : baseAmount; - const isOverdue = nextInstallment.status === 'MISSED'; - const isSelected = selectedInstallment === nextInstallment.installmentNumber; - - return ( -
- - + + ); + })()}
- ); - })()} +
+ )} -{/* Error Display */} + {/* Error Display */} {error && ( -
-

{error}

+
+ {error}
- )} + )} {/* Progress Steps */} {(isApproving || isRepaying) && selectedInstallment && ( -
-
- -

+

+
+
+ +
+
{currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} {currentStep === 'syncing' && 'Syncing with backend...'} -

+
)} @@ -411,8 +431,8 @@ export const RepayLoanModal = ({ {/* Info */} {!selectedInstallment && (
-

- Click the installment above to proceed +

+ Select an installment above to continue

)} @@ -422,11 +442,11 @@ export const RepayLoanModal = ({ {/* Footer */} {!success && ( -
+
@@ -434,9 +454,9 @@ export const RepayLoanModal = ({
)} diff --git a/src/components/portfolio/TradesTable.tsx b/src/components/portfolio/TradesTable.tsx index 25a3bf5..53a15e9 100644 --- a/src/components/portfolio/TradesTable.tsx +++ b/src/components/portfolio/TradesTable.tsx @@ -126,7 +126,7 @@ export const TradesTable = ({ <>
{/* Filter Bar */} -
+
{/* Type Filter */}
diff --git a/src/lib/api/notification.service.ts b/src/lib/api/notification.service.ts index c4524e8..9ed1946 100644 --- a/src/lib/api/notification.service.ts +++ b/src/lib/api/notification.service.ts @@ -158,8 +158,8 @@ class NotificationService extends BaseService { // Return cached data if available and not expired const now = Date.now(); const isCacheValid = this.cachedNotifications !== null && - (now - this.cacheTimestamp) < this.CACHE_TTL && - !forceRefresh; + (now - this.cacheTimestamp) < this.CACHE_TTL && + !forceRefresh; if (isCacheValid && offset === 0) { console.log('📦 Returning cached notifications (preventing duplicate fetch)'); @@ -433,7 +433,7 @@ class NotificationService extends BaseService { const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found for SSE connection'); - return () => {}; + return () => { }; } const connectSSE = async () => { @@ -443,7 +443,7 @@ class NotificationService extends BaseService { try { const url = `${this.baseURL}/notifications/stream`; console.log(`🔄 Connecting to Notification Stream at: ${url}`); - + // Match BaseService headers and required SSE headers // Removing ngrok-skip-browser-warning as it might cause CORS issues on localhost if not allowed const headers: HeadersInit = { @@ -459,15 +459,15 @@ class NotificationService extends BaseService { }); if (!response.ok) { - const errorText = await response.text().catch(() => 'No error details'); - throw new Error(`SSE Connection Failed: ${response.status} ${response.statusText} - ${errorText}`); + const errorText = await response.text().catch(() => 'No error details'); + throw new Error(`SSE Connection Failed: ${response.status} ${response.statusText} - ${errorText}`); } - + this.sseReader = response.body?.getReader() || null; if (!this.sseReader) throw new Error('ReadableStream not supported'); console.log('✅ SSE Connected'); - + const decoder = new TextDecoder(); let buffer = ''; @@ -482,7 +482,7 @@ class NotificationService extends BaseService { let currentEvent = 'message'; let currentData = ''; - + for (const line of lines) { if (line.trim() === '') { // End of event dispatch @@ -532,10 +532,10 @@ class NotificationService extends BaseService { if (error.name !== 'AbortError') { console.error('❌ SSE Error:', error); if (this.isSSEConnected) { - this.reconnectTimeout = setTimeout(() => { - console.log('♻️ Reconnecting SSE...'); - connectSSE(); - }, 5000); + this.reconnectTimeout = setTimeout(() => { + console.log('♻️ Reconnecting SSE...'); + connectSSE(); + }, 5000); } } } finally { @@ -570,17 +570,22 @@ class NotificationService extends BaseService { const roleFilters: Record = { ORIGINATOR: ['ASSET_STATUS', 'TOKEN_DEPLOYED'], INVESTOR: [ + 'ASSET_STATUS', 'KYC_STATUS', + 'YIELD_DISTRIBUTED', + 'PAYOUT_SETTLED', + 'TOKEN_PURCHASED', + 'TOKEN_DEPLOYED', + 'SYSTEM_ALERT', + 'MARKETPLACE_LISTING', 'BID_PLACED', 'AUCTION_WON', 'BID_REFUNDED', - 'TOKEN_PURCHASED', - 'YIELD_DISTRIBUTED', - 'ORDER_CANCELLED', - 'ORDER_CREATED', 'ORDER_FILLED', + 'ORDER_CANCELED', 'ORDER_ACTIVE', - 'ASSET_STATUS', + 'ORDER_CREATED', + 'ORDER_CANCELLED', ], ADMIN: ['ASSET_STATUS', 'YIELD_DISTRIBUTED'], }; From b607b6446e6d2fef4bf1564c0e6fc9f7d3607ffa Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 02:01:50 +0530 Subject: [PATCH 51/65] text color update for the email input --- src/pages/public/auth/Auth.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/public/auth/Auth.page.tsx b/src/pages/public/auth/Auth.page.tsx index 5104def..2cfb384 100644 --- a/src/pages/public/auth/Auth.page.tsx +++ b/src/pages/public/auth/Auth.page.tsx @@ -221,7 +221,7 @@ export default function AuthPage() { placeholder="Enter your email" value={email} onChange={handleEmailChange} - className="border-none bg-transparent text-white font-sans rounded-2xl h-10" + className="border-none bg-transparent text-black font-sans rounded-2xl h-10" />
From 81a1a9d03d0c7fdaecdd77f1f6151e2db2c2b0a4 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 02:02:50 +0530 Subject: [PATCH 52/65] feat: Add liquidation status and transaction handling in PositionsTable and leverage types --- src/components/leverage/PositionsTable.tsx | 51 +++++++++++++++------- src/types/leverage.types.ts | 1 + 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/components/leverage/PositionsTable.tsx b/src/components/leverage/PositionsTable.tsx index 37657d9..fced939 100644 --- a/src/components/leverage/PositionsTable.tsx +++ b/src/components/leverage/PositionsTable.tsx @@ -13,7 +13,7 @@ interface PortfolioLeveragePosition { assetId: string; tokenAddress: string; totalAmount: string; - status: 'ACTIVE' | 'SETTLED'; + status: 'ACTIVE' | 'SETTLED' | 'LIQUIDATED'; createdAt: string; firstPurchase: string; metadata: { @@ -29,8 +29,9 @@ interface PortfolioLeveragePosition { totalInterestPaid?: string; lastHarvestTime?: string; settlementTxHash?: string; + liquidationTxHash?: string; leverageInfo: { - type: 'ACTIVE' | 'SETTLED'; + type: 'ACTIVE' | 'SETTLED' | 'LIQUIDATED'; mETHCollateralFormatted: string; usdcBorrowedFormatted: string; healthFactorFormatted?: string; @@ -44,6 +45,8 @@ interface PortfolioLeveragePosition { mETHReturnedFormatted?: string; settlementTxHash?: string; settlementDate?: string; + liquidationTxHash?: string; + liquidationDate?: string; }; } @@ -58,14 +61,19 @@ interface PositionsTableProps { export const PositionsTable = ({ positions, isLoading, onSelectPosition }: PositionsTableProps) => { const navigate = useNavigate(); - const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'SETTLED' | 'LIQUIDATABLE'>('ALL'); + const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'SETTLED' | 'LIQUIDATED'>('ALL'); + + // Helper to get status + const getStatus = (pos: Position): string => { + return pos.status; + }; // Filter positions based on selected status const filteredPositions = statusFilter === 'ALL' ? positions : positions.filter(pos => getStatus(pos) === statusFilter); - const statusOptions: Array<'ALL' | 'ACTIVE' | 'SETTLED' | 'LIQUIDATABLE'> = ['ALL', 'ACTIVE', 'SETTLED', 'LIQUIDATABLE']; + const statusOptions: Array<'ALL' | 'ACTIVE' | 'SETTLED' | 'LIQUIDATED'> = ['ALL', 'ACTIVE', 'SETTLED', 'LIQUIDATED']; // Type guard to check if position is from Portfolio API const isPortfolioPosition = (pos: Position): pos is PortfolioLeveragePosition => { @@ -88,11 +96,6 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit return 'N/A'; }; - // Helper to get status - const getStatus = (pos: Position): string => { - return pos.status; - }; - // Helper to get health factor const getHealthFactor = (pos: Position): number => { if (isPortfolioPosition(pos)) { @@ -109,6 +112,13 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit return pos.settlementTxHash; }; + const getLiquidationTxHash = (pos: Position): string | undefined => { + if (isPortfolioPosition(pos)) { + return pos.liquidationTxHash || pos.leverageInfo?.liquidationTxHash; + } + return pos.liquidationTxHash; + }; + // Helper to get user yield const getUserYield = (pos: Position): string | undefined => { if (isPortfolioPosition(pos)) { @@ -183,8 +193,8 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit key={status} onClick={() => setStatusFilter(status)} className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${statusFilter === status - ? 'bg-blue-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + ? 'bg-blue-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }`} > {status} @@ -230,11 +240,12 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit const health = getHealthFactor(pos); const collateral = formatMETH(pos.mETHCollateral); const invested = formatUSDC(pos.usdcBorrowed); - const totalAmount = isPortfolioPosition(pos) ? formatMETH(pos.totalAmount) : (parseFloat(formatMETH((pos as LeveragePosition).rwaTokenAmount || '0' ))).toFixed(2); + const totalAmount = isPortfolioPosition(pos) ? formatMETH(pos.totalAmount) : (parseFloat(formatMETH((pos as LeveragePosition).rwaTokenAmount || '0'))).toFixed(2); const status = getStatus(pos); const positionStatusStyle = getPositionStatusStyle(status); const isActivePosition = status === 'ACTIVE'; const settlementTx = getSettlementTxHash(pos); + const liquidationTxHash = getLiquidationTxHash(pos); const userYield = getUserYield(pos); return ( @@ -279,7 +290,7 @@ export const PositionsTable = ({ positions, isLoading, onSelectPosition }: Posit {/* Health Factor */}
-
+
{health > 0 ? (health / 100).toFixed(2) + '%' : 'N/A'}
- + {portfolio.map((asset) => ( - asset.status !== 'CLAIMED' && asset.purchaseType !== 'LEVERAGE' && ( + asset.status !== 'CLAIMED' && asset.purchaseType !== 'LEVERAGE' && !asset.yieldInfo?.settlementDistributed && ( handleAssetSelect(asset)} From ae2dbfcc498e8bb422109fa7ced862442b836ef4 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 17:47:47 +0530 Subject: [PATCH 61/65] optimised the portfolio api to use the props --- .claude/Master_task.md | 236 ++++++++++++++++++++++ src/components/portfolio/MyLoansTable.tsx | 25 +-- src/pages/portfolio/Portfolio.page.tsx | 1 + 3 files changed, 242 insertions(+), 20 deletions(-) create mode 100644 .claude/Master_task.md diff --git a/.claude/Master_task.md b/.claude/Master_task.md new file mode 100644 index 0000000..d5c7114 --- /dev/null +++ b/.claude/Master_task.md @@ -0,0 +1,236 @@ +# 🎯 ULTIMATE TASK EXECUTION FRAMEWORK + +--- + +## 📌 INSTRUCTION TO AI: + +**Role Assignment:** You are acting as an expert [automatically determine based on task: Software Engineer / Strategic Consultant / Technical Writer / Data Analyst / Creative Director / etc.]. Your goal is to execute the task I've described above with 100% accuracy and zero context loss. + +--- + +## 🔍 PHASE 1: DEEP ANALYSIS & CONCEPTUALIZATION + +Before providing any solution, you must: + +### 1.1 Task Decomposition +- **Core Requirements:** Extract and list every explicit requirement +- **Implicit Needs:** Identify unstated but necessary elements +- **Edge Cases:** Anticipate potential pitfalls, errors, or exceptions +- **Success Criteria:** Define what "perfect completion" looks like +- **Constraints Analysis:** Identify technical, time, or resource limitations + +### 1.2 Conceptual Foundation +- **Underlying Principles:** What fundamental concepts apply? +- **Schools of Thought:** What are different approaches to this problem? +- **Domain Knowledge:** What specialized knowledge is required? +- **Dependencies:** What prerequisites or connections exist? + +### 1.3 Best-in-Class Research +- **Industry Standards:** What's the current gold standard? +- **Modern Techniques:** What are cutting-edge approaches? +- **Proven Methods:** What has consistently worked well? +- **Anti-Patterns:** What approaches should be avoided? + +### 1.4 Context Preservation Strategy +- **Information Hierarchy:** Map what information is critical vs. supplementary +- **State Tracking:** Identify what needs to be remembered throughout execution +- **Checkpoint Planning:** Determine where to pause for verification +- **Continuity Plan:** How to maintain context if conversation extends + +--- + +## 📋 PHASE 2: STRATEGIC PROPOSAL (WAIT FOR APPROVAL) + +Present a comprehensive execution plan: + +### 2.1 Recommended Approach +**Chosen Technique/Method:** +- What specific approach will you use? +- Why is it superior to alternatives? +- What are the trade-offs? + +**Alternative Approaches Considered:** +- List 2-3 other viable methods +- Explain why they were not selected + +### 2.2 Detailed Execution Roadmap + +**High-Level Structure:** +1. [Major Phase 1] +2. [Major Phase 2] +3. [Major Phase 3] + +**Granular Implementation Steps:** + +For each major phase, break down into: +- **Sub-step A.1:** [Specific action] + - Input required: [What's needed] + - Output produced: [What's created] + - Validation check: [How to verify] + - Potential issues: [What could go wrong] + +- **Sub-step A.2:** [Next action] + - [Same detailed breakdown] + +### 2.3 Quality Assurance Plan +- **Validation Points:** Where to check for errors +- **Testing Strategy:** How to verify correctness +- **Review Criteria:** What to evaluate at completion +- **Iteration Protocol:** How to handle needed revisions + +### 2.4 Context Continuity Measures +- **Critical Information to Preserve:** [List key data points] +- **State Checkpoints:** [Where to save progress] +- **Recovery Points:** [How to resume if interrupted] +- **Memory Anchors:** [Key references to maintain] + +### 2.5 Refinement Questions + +Ask 3-5 targeted questions to eliminate ambiguity: + +**Clarity Questions:** +- [Question about unclear requirements] + +**Optimization Questions:** +- [Question about preferences or priorities] + +**Validation Questions:** +- [Question to confirm understanding] + +**Scope Questions:** +- [Question about boundaries or extent] + +--- + +## ⚠️ MANDATORY CHECKPOINT + +**STOP HERE. DO NOT PROCEED TO IMPLEMENTATION.** + +Present the complete Phase 2 analysis and wait for: +- ✅ **Approval:** "Approved, proceed" +- 🔄 **Modifications:** Feedback on the strategy +- ❓ **Clarifications:** Answers to refinement questions + +--- + +## ⚙️ PHASE 3: PRECISION IMPLEMENTATION + +Once approved, execute with these protocols: + +### 3.1 Execution Standards +- **Follow the Roadmap:** Implement exactly as planned +- **Technical Proficiency:** Apply best practices throughout +- **Code/Content Quality:** Maintain professional standards +- **Completeness:** Address every sub-step identified + +### 3.2 Implementation Workflow + +**For each major phase:** + +1. **Pre-Step Verification** + - Confirm all inputs are available + - State what you're about to do + +2. **Execution** + - Implement the step precisely + - Document any deviations (with justification) + +3. **Post-Step Validation** + - Verify output meets criteria + - Confirm ready for next step + +4. **Context Update** + - Note what was accomplished + - Preserve critical state information + +### 3.3 Artifact Decision Protocol + +**Create an Artifact when:** +- Code exceeds 20 lines +- Content is meant for external use +- Document is reference material +- User will need to save/copy it +- Multiple iterations expected + +**Stay Conversational when:** +- Providing guidance or explanation +- Asking clarification questions +- Discussing strategy or approach +- Brief answers or summaries + +### 3.4 Continuous Quality Checks +- Verify each component before moving forward +- Cross-reference against original requirements +- Check for internal consistency +- Validate against success criteria + +--- + +## 🔄 PHASE 4: DELIVERY & ITERATION + +### 4.1 Final Delivery Package + +Present: +- **Primary Deliverable:** [Code/Document/Analysis/etc.] +- **Implementation Summary:** What was built and how +- **Verification Report:** Confirmation of all requirements met +- **Usage Instructions:** How to use/deploy/apply the solution +- **Next Steps:** Recommended follow-up actions (if any) + +### 4.2 Post-Delivery Support + +**Offer:** +- Clarification on any aspect +- Modifications or refinements +- Alternative approaches if needed +- Extended functionality suggestions + +### 4.3 Context Preservation for Future Reference + +**Document:** +- Key decisions made and why +- Important patterns or techniques used +- Lessons learned or insights gained +- Foundation for potential future iterations + +--- + +## 🧠 CONTINUOUS CONTEXT MANAGEMENT + +Throughout all phases: + +✓ **Maintain Working Memory** +- Keep all critical requirements active +- Track progress through the roadmap +- Remember previous decisions and rationale +- Preserve user preferences and constraints + +✓ **Progressive Disclosure** +- Don't overwhelm with everything at once +- Layer information logically +- Build on previous understanding +- Confirm comprehension before advancing + +✓ **Intelligent Checkpointing** +- Summarize progress at natural breakpoints +- Restate context when resuming after interruption +- Link current work to overall goal +- Flag when approaching complexity limits + +--- + +## 📊 SUCCESS METRICS + +The task is complete when: + +- [ ] All explicit requirements fulfilled +- [ ] All implicit needs addressed +- [ ] Edge cases handled appropriately +- [ ] Quality standards met or exceeded +- [ ] Deliverable is immediately usable +- [ ] Context fully preserved for future reference +- [ ] User confirms satisfaction + +--- + +**NOW BEGIN PHASE 1 ANALYSIS OF MY TASK ABOVE ⬆️** \ No newline at end of file diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 6272ee5..1b57cad 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -13,7 +13,7 @@ * - Filters: All, Healthy, At Risk, Critical, Defaulted */ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { createPortal } from 'react-dom'; import { ChevronDown, ChevronUp, Filter, Calendar, AlertCircle, Info, Clock, DollarSign, X, Activity, Layers, Timer } from 'lucide-react'; @@ -23,7 +23,7 @@ import { solvencyContractService } from '../../lib/api/solvency-contract.service import { format } from 'date-fns'; import { RepayLoanModal } from './RepayLoanModal'; import { PageLoader } from '../ui/page-loader'; -import { portfolioService, type PortfolioAsset } from '../../lib/api/portfolio.service'; +import { type PortfolioAsset } from '../../lib/api/portfolio.service'; import { marketplaceService } from '../../lib/api/marketplace.service'; import { ToastContainer } from '../ui/toast'; import { useToast } from '../../hooks/useToast'; @@ -33,6 +33,7 @@ interface MyLoansTableProps { positions: Position[]; isLoading: boolean; onRefresh?: () => void; + portfolioAssets: PortfolioAsset[]; } // Format USD from 6 decimal string @@ -120,7 +121,7 @@ interface LoanSchedule { }>; } -export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTableProps) => { +export const MyLoansTable = ({ positions, isLoading, onRefresh, portfolioAssets }: MyLoansTableProps) => { const navigate = useNavigate(); const [activeFilter, setActiveFilter] = useState('all'); const [expandedRows, setExpandedRows] = useState>(new Set()); @@ -132,22 +133,6 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr const [selectedPosition, setSelectedPosition] = useState(null); const [withdrawingPositionId, setWithdrawingPositionId] = useState(null); const [withdrawnPositions, setWithdrawnPositions] = useState>(new Set()); - const [portfolio, setPortfolio] = useState([]); - - useEffect(() => { - fetchPortfolio(); - }, []); - - const fetchPortfolio = async () => { - try { - const data = await portfolioService.getPortfolio(); - setPortfolio(data.portfolio); - - } catch (err: any) { - console.error('Error fetching portfolio:', err); - } finally { - } - }; // Filter positions const filteredPositions = useMemo(() => { @@ -252,7 +237,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr // Mark position as withdrawn setWithdrawnPositions(prev => new Set([...prev, selectedPosition.positionId])); - const asset = portfolio.find(asset => asset.tokenAddress === selectedPosition.collateralTokenAddress); + const asset = portfolioAssets.find(asset => asset.tokenAddress === selectedPosition.collateralTokenAddress); if (asset && asset.assetId) { await marketplaceService.notifyPurchase({ assetId: asset.assetId, diff --git a/src/pages/portfolio/Portfolio.page.tsx b/src/pages/portfolio/Portfolio.page.tsx index 3cd8fa1..c0e5b9c 100644 --- a/src/pages/portfolio/Portfolio.page.tsx +++ b/src/pages/portfolio/Portfolio.page.tsx @@ -678,6 +678,7 @@ const PortfolioPage = () => { positions={filteredLoans} isLoading={isLoadingMyLoans} onRefresh={fetchMyLoans} + portfolioAssets={allPortfolioItems} /> From 6e8f0557579d6799fa2de810ff569992c80b4d08 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 21:08:50 +0530 Subject: [PATCH 62/65] feat: Refactor waitForTransaction method for improved transaction confirmation handling and error reporting --- src/lib/api/contract.service.ts | 86 ++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/src/lib/api/contract.service.ts b/src/lib/api/contract.service.ts index 7bc0817..4cac7ff 100644 --- a/src/lib/api/contract.service.ts +++ b/src/lib/api/contract.service.ts @@ -85,35 +85,34 @@ class ContractService { * Polls every 10 seconds for up to 20 minutes * Prevents UI freeze and aggressive RPC polling */ - async waitForTransaction(txHash: string, provider: ethers.Provider): Promise { - const POLL_INTERVAL = 5000; // 5 seconds - const MAX_ATTEMPTS = 240; // 20 minutes (240 * 5s = 1200s) - - console.log(`⏳ Polling for TX ${txHash} (Interval: 5s, Timeout: 20m)...`); - - for (let i = 0; i < MAX_ATTEMPTS; i++) { - try { - const receipt = await provider.getTransactionReceipt(txHash); - if (receipt) { - if (receipt.status === 1) { - console.log(`✅ TX Confirmed in block ${receipt.blockNumber} after ${(i + 1) * 5}s`); - return receipt; - } else { - throw new Error(`Transaction failed (status: 0)`); - } - } - } catch (error: any) { - // Ignore "not found" errors during polling, rethrow others if critical - if (error.message && !error.message.includes('not found')) { - console.warn(`Polling error (attempt ${i + 1}):`, error); + async waitForTransaction( + txHash: string, + provider: ethers.Provider + ): Promise { + console.log(`⏳ Waiting for TX ${txHash} using ethers.provider.waitForTransaction...`); + try { + // Wait for 1 confirmation, with a 2-minute timeout. + // ethers.js will use an efficient combination of WebSockets and polling. + const receipt = await provider.waitForTransaction(txHash, 1, 120000); + + if (receipt) { + if (receipt.status === 1) { + console.log(`✅ TX Confirmed in block ${receipt.blockNumber}`); + return receipt; + } else { + console.error(`❌ Transaction failed (status: 0)`); + throw new Error(`Transaction failed with status 0`); } + } else { + // This case happens if the transaction is dropped from the mempool + console.warn(`⚠️ Transaction ${txHash} was not mined and may have been dropped.`); + return null; } - - // Wait for next poll - await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); + } catch (error: any) { + console.error(`Error waiting for transaction ${txHash}:`, error.message); + // Re-throw to be caught by the calling function + throw error; } - - throw new Error(`Transaction confirmation timed out after 20 minutes. TX: ${txHash}`); } /** @@ -243,6 +242,11 @@ class ContractService { console.log('Approve TX:', tx.hash); const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('Approval transaction failed to confirm and may have been dropped.'); + } + console.log('✅ USDC approved'); return { @@ -620,10 +624,15 @@ class ContractService { // Wait for confirmation const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('Purchase transaction failed to confirm and may have been dropped.'); + } + console.log(`✅ Confirmed in block ${receipt.blockNumber}`); console.log('\n✅ Purchase Complete!'); console.log('━'.repeat(50)); - console.log(`Explorer: https://explorer.sepolia.mantle.xyz/tx/${tx.hash}`); + console.log(`Explorer: https://explorer.sepolia.mantle.xyz/tx/${receipt.hash}`); return { success: true, @@ -932,6 +941,11 @@ class ContractService { console.log('⏳ Waiting for confirmation...'); const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('YieldVault approval transaction failed to confirm and may have been dropped.'); + } + console.log('✅ Approved in block', receipt.blockNumber); console.log(); @@ -1011,6 +1025,10 @@ class ContractService { console.log('⏳ Waiting for confirmation...'); const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('YieldVault approval transaction failed to confirm and may have been dropped.'); + } console.log(`✅ Confirmed in block ${receipt.blockNumber}`); console.log(); @@ -1138,9 +1156,14 @@ class ContractService { console.log('⏳ Waiting for confirmation...'); const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('Marketplace approval transaction failed to confirm and may have been dropped.'); + } + console.log(`✅ Confirmed in block ${receipt.blockNumber}`); console.log('✅ Marketplace approved!'); - console.log('Explorer:', `https://explorer.sepolia.mantle.xyz/tx/${tx.hash}`); + console.log('Explorer:', `https://explorer.sepolia.mantle.xyz/tx/${receipt.hash}`); console.log(); console.log('✅ Marketplace can now transfer tokens to buyers!'); @@ -1186,11 +1209,16 @@ class ContractService { console.log('Waiting for confirmation...'); const receipt = await this.waitForTransaction(tx.hash, provider); + + if (!receipt) { + throw new Error('End auction transaction failed to confirm and may have been dropped.'); + } + console.log('Confirmed in block', receipt.blockNumber); return { success: true, - transactionHash: tx.hash, + transactionHash: receipt.hash, blockNumber: receipt.blockNumber, clearingPriceWei: clearingPriceWei.toString(), }; From db5a452fee0f643c066a1c58b0e557df459fe235 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Thu, 15 Jan 2026 13:52:46 +0530 Subject: [PATCH 63/65] feat: Enhance completePurchase method with status update callback and improve purchase flow handling --- src/lib/api/contract.service.ts | 11 ++- .../marketplace/asset/AssetDetails.page.tsx | 70 +++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/lib/api/contract.service.ts b/src/lib/api/contract.service.ts index 4cac7ff..fb2f331 100644 --- a/src/lib/api/contract.service.ts +++ b/src/lib/api/contract.service.ts @@ -655,7 +655,11 @@ class ContractService { * Complete purchase flow: Verify + Approve USDC + Buy Tokens * Combines all steps into a single function */ - async completePurchase(params: PurchaseParams, tokenAddress?: string): Promise<{ + async completePurchase( + params: PurchaseParams, + tokenAddress?: string, + onStatusUpdate?: (status: string) => void + ): Promise<{ success: boolean; approvalTxHash?: string; purchaseTxHash?: string; @@ -665,6 +669,7 @@ class ContractService { try { // Step 0: Verify listing is available console.log('Step 0: Verifying listing...'); + onStatusUpdate?.('Intiating Purchase...'); const verification = await this.verifyListing(params.assetId, tokenAddress); if (!verification.isValid) { @@ -675,6 +680,7 @@ class ContractService { } console.log('Listing verified:', verification.details); + onStatusUpdate?.('Approving USDC...'); // Step 1: Approve USDC with correct assetId console.log('Step 1: Approving USDC...'); @@ -687,6 +693,9 @@ class ContractService { }; } + console.log('USDC approved, proceeding to buy tokens...'); + onStatusUpdate?.('USDC approved! Buying tokens...'); + // Step 2: Buy tokens with correct assetId console.log('Step 2: Buying tokens...'); const purchaseResult = await this.buyTokens(params, verification.correctAssetId); diff --git a/src/pages/marketplace/asset/AssetDetails.page.tsx b/src/pages/marketplace/asset/AssetDetails.page.tsx index b8a55be..e69feb8 100644 --- a/src/pages/marketplace/asset/AssetDetails.page.tsx +++ b/src/pages/marketplace/asset/AssetDetails.page.tsx @@ -17,6 +17,7 @@ import { useLeverageStore } from '../../../stores/leverage.store'; import { parseUnits } from 'viem'; import { LEVERAGE_CONTRACTS, METH_ABI } from '../../../lib/blockchain/leverage.contract'; import { PageLoader } from '../../../components/ui/page-loader'; +import { set } from 'date-fns'; // USDC Contract Address const USDC_ADDRESS = (import.meta.env.VITE_USDC_ADDRESS || '0x9A54Bad93a00Bf1232D4e636f5e53055Dc0b8238') as `0x${string}`; @@ -52,6 +53,8 @@ const AssetDetailsPage = () => { const [leverageTokenInput, setLeverageTokenInput] = useState(''); const [isApproving, setIsApproving] = useState(false); const [leveragePurchaseStatus, setLeveragePurchaseStatus] = useState(null); + const [leverageSuccess, setLeverageSuccess] = useState(null); + const [purchaseSuccess, setPurchaseSuccess] = useState(null); // Calculate required mETH based on token input // Formula: Required mETH = (Tokens * TokenPrice * 1.5) / mETHPrice @@ -114,6 +117,31 @@ const AssetDetailsPage = () => { // Format mETH balance (18 decimals) const methBalance = methBalanceRaw ? formatUnits(methBalanceRaw, 18) : '0'; + + const fetchPurchaseData = async () => { + setIsLoadingHistory(true); + setHistoryError(null); + if(!assetId) { + console.log('No assetId found for purchase history fetch'); + return; + } + try { + const history = await marketplaceService.getPurchaseHistory(assetId); + setPurchaseHistory(history); + + if (history.chartData && history.chartData.length > 0) { + // Aggregate purchases into 5-minute time blocks + const aggregatedData = aggregateIntoTimeBlocks(history.chartData, 0.05); + setFormattedChartData(aggregatedData); + } + + } catch (err: any) { + setHistoryError(err.message || 'Failed to fetch purchase history'); + } finally { + setIsLoadingHistory(false); + } + }; + // Load wallet data (refetch balances and allowances) const loadWalletData = useCallback(async () => { if (!address) return; @@ -140,6 +168,10 @@ const AssetDetailsPage = () => { loadWalletData(); } + if (assetId) { + fetchPurchaseData(); + } + // Auto-refresh mETH price every 30 seconds ONLY if active tab is leverage let interval: NodeJS.Timeout; if (activeTab === 'leverage') { @@ -151,7 +183,9 @@ const AssetDetailsPage = () => { return () => { if (interval) clearInterval(interval); }; - }, [assetId, address, fetchAssetDetails, fetchMethPrice, loadWalletData, activeTab, purchaseStatus,]); + }, [assetId, address, fetchAssetDetails, fetchMethPrice, loadWalletData]); + + useEffect(() => { if(assetId) fetchMethPrice(); }, [activeTab, assetId, fetchMethPrice]); useEffect(() => { if (address) { @@ -161,28 +195,13 @@ const AssetDetailsPage = () => { useEffect(() => { if (assetId) { - const fetchPurchaseData = async () => { - setIsLoadingHistory(true); - setHistoryError(null); - try { - const history = await marketplaceService.getPurchaseHistory(assetId); - setPurchaseHistory(history); - - if (history.chartData && history.chartData.length > 0) { - // Aggregate purchases into 5-minute time blocks - const aggregatedData = aggregateIntoTimeBlocks(history.chartData, 0.05); - setFormattedChartData(aggregatedData); - } - - } catch (err: any) { - setHistoryError(err.message || 'Failed to fetch purchase history'); - } finally { - setIsLoadingHistory(false); - } - }; + + fetchAssetDetails(assetId); + if(purchaseSuccess || leverageSuccess) { fetchPurchaseData(); + set } - }, [assetId]); + }}, [assetId, purchaseSuccess, leverageSuccess, fetchAssetDetails]); /** * Aggregate purchase data into time blocks @@ -256,6 +275,8 @@ const AssetDetailsPage = () => { const handleOpenLeveragePosition = async () => { if (!address || !asset || !leverageTokenInput || calculatedMethAmount <= 0) return; + setLeverageSuccess(false); + console.log('\n🚀 ===== STARTING LEVERAGED PURCHASE FLOW ====='); console.log('Asset ID:', asset.assetId); console.log('Token Amount:', leverageTokenInput); @@ -431,6 +452,8 @@ const AssetDetailsPage = () => { console.log(` Transaction Hash: ${result.transactionHash}`); console.log(` Explorer: https://explorer.sepolia.mantle.xyz/tx/${result.transactionHash}`); + + setLeverageSuccess(true); // ============================================================ // STEP 6: Monitor Position Health // ============================================================ @@ -522,6 +545,7 @@ const AssetDetailsPage = () => { const handleBuyTokens = async () => { + setPurchaseSuccess(false); if (!address) { setPurchaseStatus('Please connect your wallet first'); return; @@ -566,7 +590,8 @@ const AssetDetailsPage = () => { assetId: asset.assetId, tokenAmount: tokensToBuy, }, - asset.token?.address || '' // Pass token address for debugging + asset.token?.address || '', // Pass token address for debugging + (status: string) => setPurchaseStatus(status) // Status update callback ); if (result.success) { @@ -601,6 +626,7 @@ const AssetDetailsPage = () => { console.log('Backend response:', backendResponse); setPurchaseStatus('Purchase and notification successful! 🎉'); + setPurchaseSuccess(true); } catch (notifyError: any) { console.error('❌ Failed to notify backend:', notifyError); setPurchaseStatus('Purchase successful! (Backend notification failed)'); From c6f540ec5c6196aa665070aa482c420d809a59a9 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Thu, 15 Jan 2026 14:16:41 +0530 Subject: [PATCH 64/65] resolved the typed issues --- src/components/portfolio/MyLoansTable.tsx | 33 +---------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index 1b57cad..51f1b08 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -269,38 +269,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh, portfolioAssets } }; - const handleConfirmPayment = async () => { - if (!selectedPosition) { - throw new Error('No position selected'); - } - - try { - // derive amount to repay (use outstanding debt string and convert to BigInt) - const debt = await solvencyContractService.getOutstandingDebt(selectedPosition.positionId); - console.log('Outstanding debt to repay:', debt); - const amountWei = BigInt(debt); - - // Step 1: Approve USDC - console.log('📝 Approving USDC for Vault...'); - const approvalResult = await solvencyContractService.approveUSDCForSeniorPool(amountWei); - - if (!approvalResult.success) { - throw new Error(approvalResult.error || 'USDC approval failed'); - } - - const repayResult = await solvencyContractService.repayLoanViaSeniorPool( - selectedPosition.positionId, - amountWei - ); - - if (!repayResult.success) { - throw new Error(repayResult.error || 'Repayment failed'); - } - } catch (err: any) { - console.error('❌ Repayment error:', err); - throw err; - } - }; + if (isLoading) { return ( From ff21ebfd3f151c22af25e8892219380fc34e1cd3 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Thu, 15 Jan 2026 15:13:22 +0530 Subject: [PATCH 65/65] corrected apy for real data --- src/pages/marketplace/Marketplace.page.tsx | 32 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/pages/marketplace/Marketplace.page.tsx b/src/pages/marketplace/Marketplace.page.tsx index 6a30561..ff07435 100644 --- a/src/pages/marketplace/Marketplace.page.tsx +++ b/src/pages/marketplace/Marketplace.page.tsx @@ -140,6 +140,32 @@ const MarketplacePage = () => { .sort((a, b) => a.timestamp - b.timestamp); }; + /** + * Calculate APY (Annual Percentage Yield) for an asset + * Returns 5% APY when no tokens are sold yet + * Otherwise calculates based on purchase price vs face value and time to maturity + */ + const calculateAPY = (listing: any): number => { + const sold = parseFloat(listing.sold || '0')/1e18; + + // When no tokens are sold yet, show fixed 5% APY + if (sold === 0) { + return 5.00; + } + + const totalSupply = parseFloat(listing.totalSupply) / 1e18; + const faceValue = parseFloat(listing.faceValue)/1e6; + + if(sold> totalSupply){ + return 0; + } + + // Annualize the return (simple annualization) + const apy = ((totalSupply - sold)/sold) * faceValue * 100; + + return Math.max(0, Math.min(100, apy)); + }; + // Convert backend listings to frontend format for display // Using REAL API data with sold percentage for progress bars const displayAssets: MarketplaceAsset[] = listings.length > 0 @@ -199,7 +225,7 @@ const MarketplacePage = () => { category: 'invoice' as const, icon: '📄', tokenPrice: pricePerToken, - yieldAPY: 8, // TODO: Backend needs to provide this - using default + yieldAPY: calculateAPY(listing), // Dynamically calculated APY maturityDays: maturityDays || "Matured", // Already validated with fallback to "Matured" above totalRaised: totalRaised, targetAmount: targetAmount, @@ -940,7 +966,7 @@ const MarketplacePage = () => {
Yield - {asset.yieldAPY}% APY + {asset.yieldAPY.toFixed(2)}% APY
@@ -1041,7 +1067,7 @@ const MarketplacePage = () => { {/* Yield */}
Asset @@ -451,7 +451,7 @@ export const DepositCollateralModal = ({ {selectedAsset && assetDetails && ( <> {/* Token Balance */} -
+
Your Balance @@ -477,7 +477,7 @@ export const DepositCollateralModal = ({ value={depositAmount} onChange={(e) => setDepositAmount(e.target.value)} placeholder="0.00" - className="w-full bg-gray-100/50 border border-neutral-200 shadow-lg rounded-xl px-4 py-4 pr-20 font-gellix text-sm text-foreground transition-all focus:ring-2 focus:ring-gray-300 outline-none placeholder:text-gray-400 hover:bg-gray-100" + className="w-full bg-gray-100/50 border border-neutral-200 shadow-sm rounded-xl px-4 py-4 pr-20 font-gellix text-sm text-foreground transition-all placeholder:text-gray-400 hover:bg-gray-100" min="0" max={tokenBalance} step="0.01" @@ -496,7 +496,7 @@ export const DepositCollateralModal = ({ {/* Collateral Preview */} {depositAmount && parseFloat(depositAmount) > 0 && !depositError && ( -
+

Collateral Value

@@ -513,7 +513,7 @@ export const DepositCollateralModal = ({ )} {/* Important Notice */} -

+

⚠️ Important: Your tokens will be locked as collateral. Maintain health factor above 110% to avoid liquidation. You can withdraw after repaying your loan.

@@ -526,8 +526,8 @@ export const DepositCollateralModal = ({ {/* Approval Progress */} {step === 'approve' && ( -
-
+
+

Step 1 of 3: Approving Tokens

@@ -549,7 +549,7 @@ export const DepositCollateralModal = ({ {/* Deposit Progress */} {step === 'deposit' && (
-
+

Step 2 of 3: Depositing Collateral

@@ -571,7 +571,7 @@ export const DepositCollateralModal = ({ {/* Syncing Progress */} {step === 'syncing' && (
-
+

Step 3 of 3: Syncing with Platform

@@ -583,7 +583,7 @@ export const DepositCollateralModal = ({ {/* Success State */} {step === 'success' && (
-
+

Deposit Successful!

diff --git a/src/pages/landing/About.page.tsx b/src/pages/landing/About.page.tsx index 820678b..a7c2d6b 100644 --- a/src/pages/landing/About.page.tsx +++ b/src/pages/landing/About.page.tsx @@ -88,9 +88,9 @@ export default function AboutPage() { }; return (
+ > @@ -193,7 +193,7 @@ export default function AboutPage() {
- ) : contributors.length > 0 ? ( + ) : ( // Render contributors dynamically contributors.map((contributor, index) => { const variantType = index % 2 === 0 ? revealVariants : revealVariants2; @@ -218,74 +218,6 @@ export default function AboutPage() { ); }) - ) : ( - // Fallback to default images if no contributors - <> - - Team member - - - Team member - - - Team member - - - Team member - - )}
diff --git a/src/pages/landing/Changelog.page.tsx b/src/pages/landing/Changelog.page.tsx index 60e9396..c4aa11c 100644 --- a/src/pages/landing/Changelog.page.tsx +++ b/src/pages/landing/Changelog.page.tsx @@ -131,8 +131,7 @@ const ChangelogPage: React.FC = () => { {/* Loading State */} {isLoadingMetrics && ( -
-
+
)} diff --git a/src/pages/secondary-marketplace/TradingEngine.page.tsx b/src/pages/secondary-marketplace/TradingEngine.page.tsx index 53ca9bf..51bd701 100644 --- a/src/pages/secondary-marketplace/TradingEngine.page.tsx +++ b/src/pages/secondary-marketplace/TradingEngine.page.tsx @@ -16,6 +16,7 @@ import { SentimentChart } from '../../components/marketplace/SentimentChart'; import { TradeChart } from '../../components/marketplace/TradeChart'; import { NotificationBell } from '../../components/notifications/NotificationBell'; import { authService } from '../../lib/api/auth.service'; +import HeroBackground from '../landing/HeroBackground'; // import { Wavy } from '../../components/ui/wavy'; // Contract addresses from environment @@ -485,8 +486,8 @@ const TradingEngineProductionPage = () => { } return (<> - -
+ +
From 3f327f7c1e59d659ea507df2ad1e47f8bb5ea373 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 14:26:31 +0530 Subject: [PATCH 56/65] done marketplace improvements --- src/index.css | 4 ++ src/pages/marketplace/Marketplace.page.tsx | 45 ++++++++++++---------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/index.css b/src/index.css index cba0b18..df800d6 100644 --- a/src/index.css +++ b/src/index.css @@ -351,6 +351,10 @@ font-family: 'Gellix', sans-serif; } + .font-gellix-original{ + font-family :'Gellix', Gellix-Fallback, sans-serif; + } + .font-beau { font-family: 'Beau Rivage', cursive; } diff --git a/src/pages/marketplace/Marketplace.page.tsx b/src/pages/marketplace/Marketplace.page.tsx index 0c78b8c..6a30561 100644 --- a/src/pages/marketplace/Marketplace.page.tsx +++ b/src/pages/marketplace/Marketplace.page.tsx @@ -411,7 +411,7 @@ const MarketplacePage = () => { } return ( -
+
{/* Top Navigation Bar */}
{/* Logo */} @@ -481,7 +481,7 @@ const MarketplacePage = () => { .carousel-track { display: flex; width: max-content; /* Ensure track is as wide as content */ - animation: scroll-left 60s linear infinite; /* Slower, smoother speed */ + animation: scroll-left 20s linear infinite; /* Slower, smoother speed */ } .carousel-track:hover { animation-play-state: paused; @@ -491,8 +491,10 @@ const MarketplacePage = () => { {/* We combine all items into one list and render it TWICE to create a seamless infinite loop without gaps. */} -
- {[...Array(2)].map((_, i) => ( +
+ {/* Fade overlay left */} +
+ {[...Array(1)].map((_, i) => (
{/* SCHEDULED AUCTIONS */} @@ -553,6 +555,9 @@ const MarketplacePage = () => { ))}
))} + + {/* Fade overlay right */} +
@@ -629,10 +634,10 @@ const MarketplacePage = () => {
{/* Section 1: Active Auctions (Replaced Featured Issuances) */}
-

+

Active Auctions

-
+
{isLoadingAuctions ? (
@@ -655,7 +660,7 @@ const MarketplacePage = () => {
{auction.metadata?.invoiceNumber || auction.assetId}
-
+
{auction.totalSupply.toLocaleString()} tokens
@@ -676,7 +681,7 @@ const MarketplacePage = () => {
{index < Math.min(auctions.length, 3) - 1 && ( -
+
)}
)) @@ -686,11 +691,11 @@ const MarketplacePage = () => { {/* Section 2: Trending Assets (Highest Sold %) - Real Data from GET /marketplace/top-grossing */}
-

+

Trending Assets

-
+
{isLoadingTrending ? (
@@ -713,7 +718,7 @@ const MarketplacePage = () => {
{asset.name || asset.assetId}
-
+
{asset.industry} · {asset.activityMetrics?.totalActivity || 0} activities
@@ -725,7 +730,7 @@ const MarketplacePage = () => { {asset.percentageSold?.toFixed(1) || 0}% Sold
- + {asset.activityMetrics?.purchaseCount || 0} purchases
@@ -743,10 +748,10 @@ const MarketplacePage = () => { {/* Section 3: Recently Verified Assets - Real Data from GET /marketplace/listings (Top 3, sorted by newest) */}
-

+

Recently Verified

-
+
{isLoading ? (
@@ -766,14 +771,14 @@ const MarketplacePage = () => { {/* Left: Asset Info */}
-
+
{/* @ts-ignore */} {listing.name || listing.assetId}
-
+
{/* @ts-ignore */} {listing.industry} · {listing.listingType}
@@ -786,14 +791,14 @@ const MarketplacePage = () => { {/* @ts-ignore */} ${formatLargeNumber(parseFloat(listing.pricePerToken || '0') / 1e6)}
-
+
per token
{index < Math.min(listings.length, 3) - 1 && ( -
+
)}
)) @@ -870,7 +875,7 @@ const MarketplacePage = () => { {/* Grid/List Content */} {viewMode === 'grid' ? ( /* Grid View */ -
+
{isLoadingCharts && filteredAssets.length > 0 ? (
@@ -884,7 +889,7 @@ const MarketplacePage = () => { return (
handleTableNavigate(asset)} className="group relative bg-white rounded-3xl border border-gray-200 overflow-hidden cursor-pointer transition-all duration-200 hover:shadow-xl" > {/* Header Section */} From cb59070450134ead7c862dc13f56a4b3026a4e82 Mon Sep 17 00:00:00 2001 From: kaushalya4s5s7 Date: Wed, 14 Jan 2026 14:46:06 +0530 Subject: [PATCH 57/65] rstricted aadhar to image only --- src/pages/public/auth/Auth.page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/public/auth/Auth.page.tsx b/src/pages/public/auth/Auth.page.tsx index 2cfb384..b7abd98 100644 --- a/src/pages/public/auth/Auth.page.tsx +++ b/src/pages/public/auth/Auth.page.tsx @@ -237,16 +237,17 @@ export default function AuthPage() { - { if (files.length > 0) { - setIsUsingTestAadhar(false); - handleDocumentUpload({ aadhaar: files[0] }); + setIsUsingTestAadhar(false); + handleDocumentUpload({ aadhaar: files[0] }); } }} value={kycDocuments.aadhaar ? [kycDocuments.aadhaar] : []} text="Upload Aadhaar Card" - > + accept="image/*" + >
Date: Wed, 14 Jan 2026 14:54:29 +0530 Subject: [PATCH 58/65] adjusted teh input placeholder ux --- src/pages/borrow/components/UnifiedBorrowModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/borrow/components/UnifiedBorrowModal.tsx b/src/pages/borrow/components/UnifiedBorrowModal.tsx index fe63ab7..68dcdab 100644 --- a/src/pages/borrow/components/UnifiedBorrowModal.tsx +++ b/src/pages/borrow/components/UnifiedBorrowModal.tsx @@ -368,10 +368,10 @@ export const UnifiedBorrowModal = ({ isOpen, onSuccess, creditData}: BorrowOnlyM type="number" value={installments} onChange={(e) => { - let value = parseInt(e.target.value, 10); - if (isNaN(value)) value = 1; + let value = parseInt(e.target.value); + if (isNaN(value)) value = 0; if (value > 24) value = 24; - if (value < 1) value = 1; + if (value < 1) value = 0; setInstallments(value); }} min="1" From 9d49c8a273c91b694b9036625d0e7ae312da04a9 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 15:04:34 +0530 Subject: [PATCH 59/65] feat: Enhance repayment process with last installment handling and update PageLoader component --- src/components/portfolio/MyLoansTable.tsx | 15 ++++----- src/components/portfolio/RepayLoanModal.tsx | 34 ++++++++++----------- src/components/ui/page-loader.tsx | 4 ++- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index f559132..f4dee62 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -207,12 +207,15 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr setShowRepayModal(true); }; - const handleRepaySuccess = () => { + const handleRepaySuccess = (isLastInstallment?: boolean) => { setShowRepayModal(false); setSelectedPosition(null); console.log('Repay successful for position', selectedPosition); console.log("withdrawing after repay"); - handleWithdrawConfirm(); + + if (isLastInstallment) { + handleWithdrawConfirm(); + } if (onRefresh) onRefresh(); @@ -247,8 +250,6 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr // Mark position as withdrawn setWithdrawnPositions(prev => new Set([...prev, selectedPosition.positionId])); - - const asset = portfolio.find(asset => asset.tokenAddress === selectedPosition.collateralTokenAddress); if (asset && asset.assetId) { await marketplaceService.notifyPurchase({ @@ -548,8 +549,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr
- - {isSelected && ( -
- -
- )}
@@ -414,12 +414,10 @@ export const RepayLoanModal = ({ {/* Progress Steps */} {(isApproving || isRepaying) && selectedInstallment && ( -
-
-
- -
-
+
+
+ +
{currentStep === 'approving' && `Approving USDC for Installment #${selectedInstallment}...`} {currentStep === 'repaying' && `Processing payment for Installment #${selectedInstallment}...`} {currentStep === 'syncing' && 'Syncing with backend...'} diff --git a/src/components/ui/page-loader.tsx b/src/components/ui/page-loader.tsx index 9344781..6df36c3 100644 --- a/src/components/ui/page-loader.tsx +++ b/src/components/ui/page-loader.tsx @@ -6,16 +6,18 @@ import { Loader } from './loader'; export interface PageLoaderProps { text?: string; + size?: 'sm' | 'md' | 'lg' | 'xl'; showText?: boolean; } const PageLoader: React.FC = ({ text = 'Loading...', + size = 'md', showText = true }) => { return (
- + {showText && (

{text} From 9c9367922128ec1131e0f63abc783b1c915ceea3 Mon Sep 17 00:00:00 2001 From: Abhinav Pangaria Date: Wed, 14 Jan 2026 15:56:41 +0530 Subject: [PATCH 60/65] feat: Implement collateral withdrawal and repayment handling in MyLoansTable; update asset filtering in DepositCollateralModal --- src/components/portfolio/MyLoansTable.tsx | 37 ++++++++++++++++++- .../components/DepositCollateralModal.tsx | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/components/portfolio/MyLoansTable.tsx b/src/components/portfolio/MyLoansTable.tsx index f4dee62..6272ee5 100644 --- a/src/components/portfolio/MyLoansTable.tsx +++ b/src/components/portfolio/MyLoansTable.tsx @@ -240,6 +240,8 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr try { // Call smart contract directly to withdraw collateral + + // await handleConfirmPayment(); const amountBigInt = BigInt(selectedPosition.collateralAmount); const result = await solvencyContractService.withdrawCollateral( selectedPosition.positionId, @@ -282,6 +284,39 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr } }; + const handleConfirmPayment = async () => { + if (!selectedPosition) { + throw new Error('No position selected'); + } + + try { + // derive amount to repay (use outstanding debt string and convert to BigInt) + const debt = await solvencyContractService.getOutstandingDebt(selectedPosition.positionId); + console.log('Outstanding debt to repay:', debt); + const amountWei = BigInt(debt); + + // Step 1: Approve USDC + console.log('📝 Approving USDC for Vault...'); + const approvalResult = await solvencyContractService.approveUSDCForSeniorPool(amountWei); + + if (!approvalResult.success) { + throw new Error(approvalResult.error || 'USDC approval failed'); + } + + const repayResult = await solvencyContractService.repayLoanViaSeniorPool( + selectedPosition.positionId, + amountWei + ); + + if (!repayResult.success) { + throw new Error(repayResult.error || 'Repayment failed'); + } + } catch (err: any) { + console.error('❌ Repayment error:', err); + throw err; + } + }; + if (isLoading) { return (

@@ -763,7 +798,7 @@ export const MyLoansTable = ({ positions, isLoading, onRefresh }: MyLoansTablePr setShowRepayModal(false); setSelectedPosition(null); }} - onSuccess={(isLastInstallment:boolean) => handleRepaySuccess(isLastInstallment)} + onSuccess={(isLastInstallment: boolean) => handleRepaySuccess(isLastInstallment)} position={selectedPosition} />, document.body diff --git a/src/pages/borrow/components/DepositCollateralModal.tsx b/src/pages/borrow/components/DepositCollateralModal.tsx index 9132a2b..6ee16b9 100644 --- a/src/pages/borrow/components/DepositCollateralModal.tsx +++ b/src/pages/borrow/components/DepositCollateralModal.tsx @@ -382,7 +382,7 @@ export const DepositCollateralModal = ({
- {asset.yieldAPY}% APY + {asset.yieldAPY.toFixed(2)}% APY