Pipeline confirmed working (17826 keyMap, ~350 geolocalized). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
/**
|
|
* 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<string, string>; expiresAt: number } | null = null;
|
|
|
|
async function getIdentityKeyMap(): Promise<Map<string, string>> {
|
|
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<string, string>();
|
|
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<string, { lat: number; lng: number; city: string }>();
|
|
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<DataResult> {
|
|
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',
|
|
};
|
|
}
|