166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
import { create } from 'zustand';
|
|
import { invoke } from '../lib/tauri';
|
|
|
|
export interface YieldOpportunity {
|
|
id: string;
|
|
name: string;
|
|
protocol: string;
|
|
asset: string;
|
|
apy: number;
|
|
tvl: number;
|
|
riskLevel: 'low' | 'medium' | 'high';
|
|
lockupPeriodDays: number;
|
|
minDeposit: number;
|
|
}
|
|
|
|
export interface YieldPosition {
|
|
id: string;
|
|
opportunityId: string;
|
|
depositedAmount: number;
|
|
currentValue: number;
|
|
rewardsEarned: number;
|
|
autoCompound: boolean;
|
|
createdAt: number;
|
|
lastCompoundAt?: number;
|
|
}
|
|
|
|
interface YieldState {
|
|
opportunities: YieldOpportunity[];
|
|
positions: YieldPosition[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface YieldActions {
|
|
loadOpportunities: () => Promise<void>;
|
|
deposit: (opportunityId: string, amount: number, autoCompound: boolean) => Promise<YieldPosition>;
|
|
withdraw: (positionId: string, amount?: number) => Promise<YieldPosition>;
|
|
listPositions: () => Promise<void>;
|
|
compound: (positionId: string) => Promise<YieldPosition>;
|
|
clearError: () => void;
|
|
}
|
|
|
|
// Transform snake_case to camelCase
|
|
const transformOpportunity = (data: Record<string, unknown>): YieldOpportunity => ({
|
|
id: data.id as string,
|
|
name: data.name as string,
|
|
protocol: data.protocol as string,
|
|
asset: data.asset as string,
|
|
apy: data.apy as number,
|
|
tvl: data.tvl as number,
|
|
riskLevel: data.risk_level as YieldOpportunity['riskLevel'],
|
|
lockupPeriodDays: data.lockup_period_days as number,
|
|
minDeposit: data.min_deposit as number,
|
|
});
|
|
|
|
const transformPosition = (data: Record<string, unknown>): YieldPosition => ({
|
|
id: data.id as string,
|
|
opportunityId: data.opportunity_id as string,
|
|
depositedAmount: data.deposited_amount as number,
|
|
currentValue: data.current_value as number,
|
|
rewardsEarned: data.rewards_earned as number,
|
|
autoCompound: data.auto_compound as boolean,
|
|
createdAt: data.created_at as number,
|
|
lastCompoundAt: data.last_compound_at as number | undefined,
|
|
});
|
|
|
|
export const useYieldStore = create<YieldState & YieldActions>((set) => ({
|
|
opportunities: [],
|
|
positions: [],
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
loadOpportunities: async () => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>[]>('yield_get_opportunities');
|
|
const opportunities = data.map(transformOpportunity);
|
|
set({ opportunities, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
deposit: async (opportunityId: string, amount: number, autoCompound: boolean) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>>('yield_deposit', {
|
|
opportunityId,
|
|
amount,
|
|
autoCompound,
|
|
});
|
|
const position = transformPosition(data);
|
|
set((state) => ({
|
|
positions: [position, ...state.positions],
|
|
isLoading: false,
|
|
}));
|
|
return position;
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
withdraw: async (positionId: string, amount?: number) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>>('yield_withdraw', {
|
|
positionId,
|
|
amount,
|
|
});
|
|
const position = transformPosition(data);
|
|
set((state) => ({
|
|
positions: state.positions.map((p) => (p.id === positionId ? position : p)),
|
|
isLoading: false,
|
|
}));
|
|
return position;
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
listPositions: async () => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>[]>('yield_list_positions');
|
|
const positions = data.map(transformPosition);
|
|
set({ positions, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
compound: async (positionId: string) => {
|
|
try {
|
|
const data = await invoke<Record<string, unknown>>('yield_compound', { positionId });
|
|
const position = transformPosition(data);
|
|
set((state) => ({
|
|
positions: state.positions.map((p) => (p.id === positionId ? position : p)),
|
|
}));
|
|
return position;
|
|
} catch (error) {
|
|
set({ error: String(error) });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
clearError: () => set({ error: null }),
|
|
}));
|
|
|
|
// Helper to format APY
|
|
export const formatAPY = (apy: number): string => `${apy.toFixed(2)}%`;
|
|
|
|
// Helper to format TVL
|
|
export const formatTVL = (sompi: number): string => {
|
|
const syn = sompi / 100_000_000;
|
|
if (syn >= 1_000_000) return `${(syn / 1_000_000).toFixed(2)}M SYN`;
|
|
if (syn >= 1_000) return `${(syn / 1_000).toFixed(2)}K SYN`;
|
|
return `${syn.toFixed(2)} SYN`;
|
|
};
|
|
|
|
// Helper to format sompi to SYN
|
|
export const formatAmount = (sompi: number): string => {
|
|
const syn = sompi / 100_000_000;
|
|
return `${syn.toFixed(8)} SYN`;
|
|
};
|