244 lines
5.9 KiB
TypeScript
244 lines
5.9 KiB
TypeScript
|
|
// Fund Market API Client
|
||
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api';
|
||
|
|
|
||
|
|
export interface StatsData {
|
||
|
|
label: string;
|
||
|
|
value: string;
|
||
|
|
change: string;
|
||
|
|
isPositive: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface Product {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
tokenSymbol: string;
|
||
|
|
decimals: number;
|
||
|
|
contractAddress: string;
|
||
|
|
chainId: number;
|
||
|
|
token_role: string;
|
||
|
|
category: string;
|
||
|
|
categoryColor: "blue" | "green" | "orange" | "purple" | "red";
|
||
|
|
iconUrl: string;
|
||
|
|
yieldAPY: string;
|
||
|
|
poolCap: string;
|
||
|
|
risk: string;
|
||
|
|
riskLevel: 1 | 2 | 3;
|
||
|
|
circulatingSupply: string;
|
||
|
|
poolCapacityPercent: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch all products
|
||
|
|
// tokenList=true: include stablecoins (for token dropdowns)
|
||
|
|
// tokenList=false (default): exclude stablecoins (Fund Market page)
|
||
|
|
export async function fetchProducts(tokenList: boolean = false): Promise<Product[]> {
|
||
|
|
try {
|
||
|
|
const url = tokenList === true
|
||
|
|
? `${API_BASE_URL}/fundmarket/products?token_list=1`
|
||
|
|
: `${API_BASE_URL}/fundmarket/products`;
|
||
|
|
const response = await fetch(url, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch products');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch products');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data ?? [];
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching products:', error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch market stats
|
||
|
|
export async function fetchStats(): Promise<StatsData[]> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/fundmarket/stats`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch stats');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch stats');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching stats:', error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Product Detail Interfaces
|
||
|
|
export interface ProductDetail {
|
||
|
|
// Basic Info
|
||
|
|
id: number;
|
||
|
|
assetCode: string;
|
||
|
|
name: string;
|
||
|
|
subtitle: string;
|
||
|
|
description: string;
|
||
|
|
tokenSymbol: string;
|
||
|
|
decimals: number;
|
||
|
|
|
||
|
|
// Investment Parameters
|
||
|
|
underlyingAssets: string;
|
||
|
|
poolCapUsd: number;
|
||
|
|
riskLevel: number;
|
||
|
|
riskLabel: string;
|
||
|
|
targetApy: number;
|
||
|
|
|
||
|
|
// Contract Info
|
||
|
|
contractAddress: string;
|
||
|
|
chainId: number;
|
||
|
|
|
||
|
|
// Display Info
|
||
|
|
category: string;
|
||
|
|
categoryColor: string;
|
||
|
|
iconUrl: string;
|
||
|
|
|
||
|
|
// Performance Data
|
||
|
|
currentApy: number;
|
||
|
|
tvlUsd: number;
|
||
|
|
volume24hUsd: number;
|
||
|
|
volumeChangeVsAvg: number;
|
||
|
|
circulatingSupply: number;
|
||
|
|
poolCapacityPercent: number;
|
||
|
|
currentPrice: number;
|
||
|
|
|
||
|
|
// Custody Info
|
||
|
|
custody?: {
|
||
|
|
custodianName: string;
|
||
|
|
custodyType: string;
|
||
|
|
custodyLocation: string;
|
||
|
|
auditorName: string;
|
||
|
|
lastAuditDate: string;
|
||
|
|
auditReportUrl?: string;
|
||
|
|
additionalInfo?: any;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Audit Reports
|
||
|
|
auditReports: Array<{
|
||
|
|
reportType: string;
|
||
|
|
reportTitle: string;
|
||
|
|
reportDate: string;
|
||
|
|
auditorName: string;
|
||
|
|
summary: string;
|
||
|
|
reportUrl: string;
|
||
|
|
}>;
|
||
|
|
|
||
|
|
// Product Links
|
||
|
|
productLinks: Array<{
|
||
|
|
linkText: string;
|
||
|
|
linkUrl: string;
|
||
|
|
description: string;
|
||
|
|
displayArea: string; // 'protocol' | 'verification' | 'both'
|
||
|
|
displayOrder: number;
|
||
|
|
}>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch single product by ID (simple format)
|
||
|
|
export async function fetchProductById(id: number): Promise<Product | null> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/fundmarket/products/${id}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch product');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch product');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching product:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Daily return point
|
||
|
|
export interface DailyReturnPoint {
|
||
|
|
date: string; // "YYYY-MM-DD"
|
||
|
|
ytPrice: number;
|
||
|
|
dailyReturn: number; // %
|
||
|
|
hasData: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fetchDailyReturns(id: number, year: number, month: number): Promise<DailyReturnPoint[]> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`${API_BASE_URL}/fundmarket/products/${id}/daily-returns?year=${year}&month=${month}`,
|
||
|
|
{ cache: 'no-store' }
|
||
|
|
);
|
||
|
|
if (!response.ok) throw new Error('Failed to fetch daily returns');
|
||
|
|
const data = await response.json();
|
||
|
|
if (!data.success) throw new Error(data.error || 'Failed to fetch daily returns');
|
||
|
|
return data.data ?? [];
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching daily returns:', error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// History point for APY/Price chart
|
||
|
|
export interface HistoryPoint {
|
||
|
|
time: string; // "MM/DD HH:mm"
|
||
|
|
apy: number; // APY %
|
||
|
|
price: number; // YT token price in USDC
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch hourly APY/price history for chart
|
||
|
|
export async function fetchProductHistory(id: number): Promise<HistoryPoint[]> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/fundmarket/products/${id}/history`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
if (!response.ok) throw new Error('Failed to fetch history');
|
||
|
|
const data = await response.json();
|
||
|
|
if (!data.success) throw new Error(data.error || 'Failed to fetch history');
|
||
|
|
return data.data ?? [];
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching product history:', error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch product detail by ID (detailed format)
|
||
|
|
export async function fetchProductDetail(id: number): Promise<ProductDetail | null> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/fundmarket/products/${id}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch product detail');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch product detail');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching product detail:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|