/** * 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, fetchCurrentUD, ss58ToDuniterKey } from './adapters/SubsquidAdapter'; import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter'; import { getTransactionsForPeriod, computeStats, type Transaction, } from '../data/mockData'; import { buildMockArcs, type TransactionArc, } from '../data/arcData'; const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true'; // Cache du DU courant, valide 1 heure (le DU change tous les ~6 mois) let udCache: { value: number; expiresAt: number } | null = null; async function getCurrentUD(): Promise { if (udCache && Date.now() < udCache.expiresAt) return udCache.value; const value = await fetchCurrentUD(); udCache = { value, expiresAt: Date.now() + 60 * 60 * 1000 }; return value; } // 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[]; arcs: TransactionArc[]; totalCount: number; totalVolume: number; allTimestamps: number[]; }> { // ~400 tx/jour sur le réseau Ğ1v2 → marge ×1.5 arrondie, minimum 2000 const limit = Math.max(2000, Math.ceil(periodDays * 600)); const { transfers: rawTransfers, totalCount } = await fetchTransfers(periodDays, limit); if (rawTransfers.length === 0) return { geolocated: [], arcs: [], totalCount: 0, totalVolume: 0, allTimestamps: [] }; 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 ET destinataires (un seul appel Cesium+) // Pour les membres WoT : via keyMap (genesis key = _id Cesium+) // Pour les non-membres : conversion directe SS58 → Duniter key const resolveKey = (ss58: string): string => keyMap.get(ss58) ?? ss58ToDuniterKey(ss58); const allDuniterKeys = [...new Set([ ...rawTransfers.map((t) => t.fromId ? resolveKey(t.fromId) : undefined), ...rawTransfers.map((t) => t.toId ? resolveKey(t.toId) : undefined), ].filter(Boolean) as string[])]; // Résolution géo par clé Duniter (_id Cesium+) let geoMap = new Map(); try { const profiles = await resolveGeoByKeys(allDuniterKeys); for (const [key, p] of profiles) { geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city, countryCode: p.countryCode }); } } catch (err) { console.warn('Cesium+ indisponible :', err); } const geolocated: Transaction[] = []; const arcs: TransactionArc[] = []; for (const t of rawTransfers) { const fromDuniterKey = t.fromId ? resolveKey(t.fromId) : undefined; if (!fromDuniterKey) continue; const fromGeo = geoMap.get(fromDuniterKey); if (!fromGeo) continue; const fromCity = cleanCityName(fromGeo.city); // Heatmap : émetteur géolocalisé geolocated.push({ id: t.id, timestamp: t.timestamp, lat: fromGeo.lat, lng: fromGeo.lng, amount: t.amount, city: fromCity, countryCode: fromGeo.countryCode, fromKey: t.fromId, toKey: t.toId, }); // Arc : les deux extrémités géolocalisées + villes différentes const toDuniterKey = t.toId ? resolveKey(t.toId) : undefined; if (!toDuniterKey) continue; const toGeo = geoMap.get(toDuniterKey); if (!toGeo) continue; const toCity = cleanCityName(toGeo.city); if (fromCity === toCity) continue; arcs.push({ id: `${t.id}-arc`, timestamp: t.timestamp, amount: t.amount, fromLat: fromGeo.lat, fromLng: fromGeo.lng, fromCity, fromCountry: fromGeo.countryCode, fromKey: t.fromId, toLat: toGeo.lat, toLng: toGeo.lng, toCity, toCountry: toGeo.countryCode, toKey: t.toId, }); } return { geolocated, arcs, totalCount, totalVolume, allTimestamps: rawTransfers.map((t) => t.timestamp) }; } // --------------------------------------------------------------------------- // 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; countryCode: string }[]; } export interface DataResult { transactions: Transaction[]; // uniquement géolocalisées → heatmap arcs: TransactionArc[]; // les deux extrémités géolocalisées → vue flux stats: PeriodStats; source: 'live' | 'mock'; currentUD: number; // valeur du DU courant en Ğ1 allTimestamps: number[]; // timestamps de TOUS les transfers (géo + non-géo) } 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); const arcs = buildMockArcs(transactions); return { transactions, arcs, stats: { ...base, geoCount: transactions.length }, source: 'mock', currentUD: 11.78, allTimestamps: transactions.map((t) => t.timestamp), }; } const [{ geolocated, arcs, totalCount, totalVolume, allTimestamps }, currentUD] = await Promise.all([ fetchLiveTransactions(periodDays), getCurrentUD(), ]); const base = computeStats(geolocated); return { transactions: geolocated, arcs, stats: { totalVolume, transactionCount: totalCount, geoCount: geolocated.length, topCities: base.topCities, }, source: 'live', currentUD, allTimestamps, }; }