mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
feat: add a Crypto tab with live CoinGecko prices synced into Net Worth
Track individual coins (identity + quantity) in a dedicated tab, backed by CoinGecko's Demo API (AURUM_COINGECKO_API_KEY). A holding is just another Asset under the hood (asset_class=crypto) with its value written as a regular AssetValuation — the existing Net Worth aggregation needs zero changes to pick it up. Prices never refresh in the background (no scheduler in this stack, same reasoning as recurring transactions): a "Refresh prices" button forces a sync, and GET /crypto/holdings lazily re-syncs at most once every 24h. CoinGecko outages leave existing values untouched (error_key: "unreachable") instead of failing the request. Backup/restore now carries CryptoHolding rows too, so a restore doesn't silently strip a holding's coin identity. Backend: 75/75 tests passing (9 new), including a real identity-map staleness bug the tests caught in the raw-Core-upsert + ORM re-read pattern, fixed via populate_existing=True. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
59
frontend/src/hooks/useCrypto.ts
Normal file
59
frontend/src/hooks/useCrypto.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createCryptoHolding,
|
||||
deleteCryptoHolding,
|
||||
fetchCryptoHoldings,
|
||||
refreshCryptoPrices,
|
||||
searchCryptoCoins,
|
||||
updateCryptoHoldingQuantity,
|
||||
} from "@/api/crypto";
|
||||
import type { CryptoHoldingInput } from "@/types";
|
||||
|
||||
function useInvalidateCrypto() {
|
||||
const queryClient = useQueryClient();
|
||||
return () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["crypto-holdings"] });
|
||||
// A crypto holding is a normal Asset under the hood — Net Worth's own
|
||||
// numbers change the moment one is added/edited/repriced/removed.
|
||||
queryClient.invalidateQueries({ queryKey: ["net-worth-summary"] });
|
||||
};
|
||||
}
|
||||
|
||||
export function useCryptoHoldings() {
|
||||
return useQuery({ queryKey: ["crypto-holdings"], queryFn: fetchCryptoHoldings });
|
||||
}
|
||||
|
||||
export function useRefreshCryptoPrices() {
|
||||
const invalidate = useInvalidateCrypto();
|
||||
return useMutation({ mutationFn: refreshCryptoPrices, onSuccess: invalidate });
|
||||
}
|
||||
|
||||
export function useCreateCryptoHolding() {
|
||||
const invalidate = useInvalidateCrypto();
|
||||
return useMutation({
|
||||
mutationFn: (input: CryptoHoldingInput) => createCryptoHolding(input),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCryptoHoldingQuantity() {
|
||||
const invalidate = useInvalidateCrypto();
|
||||
return useMutation({
|
||||
mutationFn: ({ assetId, quantity }: { assetId: number; quantity: string }) =>
|
||||
updateCryptoHoldingQuantity(assetId, quantity),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCryptoHolding() {
|
||||
const invalidate = useInvalidateCrypto();
|
||||
return useMutation({
|
||||
mutationFn: (assetId: number) => deleteCryptoHolding(assetId),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
// Not a useQuery — this drives a search-as-you-type picker inside the "add
|
||||
// holding" modal, called imperatively (debounced) rather than kept in sync
|
||||
// with a query key.
|
||||
export { searchCryptoCoins };
|
||||
Reference in New Issue
Block a user