synor/apps/desktop-wallet/src/store/portfolio.ts
2026-02-02 15:16:29 +05:30

189 lines
5.4 KiB
TypeScript

import { create } from 'zustand';
import { invoke } from '../lib/tauri';
export interface PortfolioSummary {
totalValueUsd: number;
totalCostBasisUsd: number;
totalPnlUsd: number;
totalPnlPercent: number;
dayChangeUsd: number;
dayChangePercent: number;
}
export interface AssetHolding {
asset: string;
symbol: string;
balance: number;
balanceFormatted: string;
priceUsd: number;
valueUsd: number;
costBasisUsd: number;
pnlUsd: number;
pnlPercent: number;
allocationPercent: number;
}
export interface TaxableTransaction {
id: string;
txType: 'buy' | 'sell' | 'swap' | 'transfer';
asset: string;
amount: number;
priceUsd: number;
totalUsd: number;
costBasisUsd?: number;
gainLossUsd?: number;
timestamp: number;
isLongTerm: boolean;
}
export interface HistoryPoint {
timestamp: number;
valueUsd: number;
}
interface PortfolioState {
summary: PortfolioSummary | null;
holdings: AssetHolding[];
taxReport: TaxableTransaction[];
history: HistoryPoint[];
isLoading: boolean;
error: string | null;
}
interface PortfolioActions {
loadSummary: () => Promise<void>;
loadHoldings: () => Promise<void>;
loadTaxReport: (year: number) => Promise<void>;
exportTaxReport: (year: number, format: 'csv' | 'txf' | 'json') => Promise<string>;
loadHistory: (days: number) => Promise<void>;
clearError: () => void;
}
// Transform snake_case to camelCase
const transformSummary = (data: Record<string, unknown>): PortfolioSummary => ({
totalValueUsd: data.total_value_usd as number,
totalCostBasisUsd: data.total_cost_basis_usd as number,
totalPnlUsd: data.total_pnl_usd as number,
totalPnlPercent: data.total_pnl_percent as number,
dayChangeUsd: data.day_change_usd as number,
dayChangePercent: data.day_change_percent as number,
});
const transformHolding = (data: Record<string, unknown>): AssetHolding => ({
asset: data.asset as string,
symbol: data.symbol as string,
balance: data.balance as number,
balanceFormatted: data.balance_formatted as string,
priceUsd: data.price_usd as number,
valueUsd: data.value_usd as number,
costBasisUsd: data.cost_basis_usd as number,
pnlUsd: data.pnl_usd as number,
pnlPercent: data.pnl_percent as number,
allocationPercent: data.allocation_percent as number,
});
const transformTaxTx = (data: Record<string, unknown>): TaxableTransaction => ({
id: data.id as string,
txType: data.tx_type as TaxableTransaction['txType'],
asset: data.asset as string,
amount: data.amount as number,
priceUsd: data.price_usd as number,
totalUsd: data.total_usd as number,
costBasisUsd: data.cost_basis_usd as number | undefined,
gainLossUsd: data.gain_loss_usd as number | undefined,
timestamp: data.timestamp as number,
isLongTerm: data.is_long_term as boolean,
});
const transformHistory = (data: Record<string, unknown>): HistoryPoint => ({
timestamp: data.timestamp as number,
valueUsd: data.value_usd as number,
});
export const usePortfolioStore = create<PortfolioState & PortfolioActions>((set) => ({
summary: null,
holdings: [],
taxReport: [],
history: [],
isLoading: false,
error: null,
loadSummary: async () => {
try {
set({ isLoading: true, error: null });
const data = await invoke<Record<string, unknown>>('portfolio_get_summary');
const summary = transformSummary(data);
set({ summary, isLoading: false });
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
loadHoldings: async () => {
try {
set({ isLoading: true, error: null });
const data = await invoke<Record<string, unknown>[]>('portfolio_get_holdings');
const holdings = data.map(transformHolding);
set({ holdings, isLoading: false });
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
loadTaxReport: async (year: number) => {
try {
set({ isLoading: true, error: null });
const data = await invoke<Record<string, unknown>[]>('portfolio_get_tax_report', { year });
const taxReport = data.map(transformTaxTx);
set({ taxReport, isLoading: false });
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
exportTaxReport: async (year: number, format: 'csv' | 'txf' | 'json') => {
try {
const data = await invoke<string>('portfolio_export_tax_report', { year, format });
return data;
} catch (error) {
set({ error: String(error) });
throw error;
}
},
loadHistory: async (days: number) => {
try {
set({ isLoading: true, error: null });
const data = await invoke<Record<string, unknown>[]>('portfolio_get_history', { days });
const history = data.map(transformHistory);
set({ history, isLoading: false });
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
clearError: () => set({ error: null }),
}));
// Helper to format currency
export const formatUSD = (value: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
};
// Helper to format percentage
export const formatPercent = (value: number): string => {
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(2)}%`;
};
// Alias for formatting USD
export const formatCurrency = formatUSD;
// Helper to format sompi to SYN
export const formatAmount = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn.toFixed(8)} SYN`;
};