/** * DataService — couche d'abstraction entre l'UI et les sources de données. * * Mode mock (USE_LIVE_API = false) : données simulées, aucun appel réseau. * Mode live (USE_LIVE_API = true) : données réelles Ğ1v2. * - Transactions : Subsquid indexer https://squidv2s.syoul.fr/v1/graphql * - Géolocalisation : Cesium+ https://g1.data.e-is.pro * → lookup par clé Duniter (_id) via conversion SS58 → base58 Ed25519 * → membres migrés v1→v2 : clé genesis (previousId) = _id Cesium+ * → carte d'identité (keyMap) mise en cache 10 min, 1 seule requête * * Pour activer : définir VITE_USE_LIVE_API=true dans .env.local */ import { fetchTransfers, buildIdentityKeyMap } from './adapters/SubsquidAdapter'; import { resolveGeoByKeys } from './adapters/CesiumAdapter'; import { getTransactionsForPeriod, computeStats, type Transaction, } from '../data/mockData'; const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true'; // Cache de la carte identité SS58→DuniterKey, valide 10 minutes let keyMapCache: { map: Map; expiresAt: number } | null = null; async function getIdentityKeyMap(): Promise> { if (keyMapCache && Date.now() < keyMapCache.expiresAt) return keyMapCache.map; const map = await buildIdentityKeyMap(); keyMapCache = { map, expiresAt: Date.now() + 10 * 60 * 1000 }; return map; } async function fetchLiveTransactions(periodDays: number): Promise<{ geolocated: Transaction[]; totalCount: number; totalVolume: number; }> { const rawTransfers = await fetchTransfers(periodDays); if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 }; const totalCount = rawTransfers.length; const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0); // Carte SS58 courant → clé Duniter (= _id Cesium+) let keyMap = new Map(); try { keyMap = await getIdentityKeyMap(); } catch (err) { console.warn('Identity key map indisponible :', err); } // Clés Duniter uniques des émetteurs const duniterKeys = [...new Set( rawTransfers.map((t) => keyMap.get(t.fromId)).filter(Boolean) as string[] )]; // Résolution géo par clé Duniter (_id Cesium+) let geoMap = new Map(); try { const profiles = await resolveGeoByKeys(duniterKeys); for (const [key, p] of profiles) { geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city }); } } catch (err) { console.warn('Cesium+ indisponible :', err); } // Seules les transactions avec un profil géo entrent dans le heatmap const geolocated: Transaction[] = []; for (const t of rawTransfers) { const duniterKey = keyMap.get(t.fromId); if (!duniterKey) continue; const geo = geoMap.get(duniterKey); if (!geo) continue; geolocated.push({ id: t.id, timestamp: t.timestamp, lat: geo.lat, lng: geo.lng, amount: t.amount, city: geo.city, fromKey: t.fromId, toKey: t.toId, }); } return { geolocated, totalCount, totalVolume }; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- export interface PeriodStats { totalVolume: number; transactionCount: number; // total blockchain (y compris non-géolocalisés) geoCount: number; // transactions visibles sur la carte topCities: { name: string; volume: number; count: number }[]; } export interface DataResult { transactions: Transaction[]; // uniquement géolocalisées → heatmap stats: PeriodStats; source: 'live' | 'mock'; } export async function fetchData(periodDays: number): Promise { if (!USE_LIVE_API) { await new Promise((r) => setTimeout(r, 80)); const transactions = getTransactionsForPeriod(periodDays); const base = computeStats(transactions); return { transactions, stats: { ...base, geoCount: transactions.length }, source: 'mock', }; } const { geolocated, totalCount, totalVolume } = await fetchLiveTransactions(periodDays); const base = computeStats(geolocated); return { transactions: geolocated, stats: { totalVolume, transactionCount: totalCount, geoCount: geolocated.length, topCities: base.topCities, }, source: 'live', }; }