feat: géolocalisation par clé cryptographique (SS58 → Duniter base58)
Au lieu de chercher par nom (title), on résout maintenant par clé :
1. buildIdentityKeyMap() : charge toutes les identités Ğ1v2 depuis Subsquid
avec leur ownerKeyChange → currentSS58 → genesisKey → duniterKey
2. ss58ToDuniterKey() : conversion SS58 v2 (préfixe 2 octets) → base58 Ed25519
= _id Cesium+ (même matériau cryptographique, encodage différent)
3. resolveGeoByKeys() : query Cesium+ par ids{} → résultat exact, pas d'ambiguïté
4. Cache keyMap 10 min : 1 requête Subsquid pour ~8000 identités, pas par refresh
Résultat : les membres migrés v1→v2 avec un profil Cesium+ sont correctement
géolocalisés même si leur nom v2 diffère de leur nom v1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,17 +3,17 @@
|
||||
*
|
||||
* 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
|
||||
* → recherche batch par nom d'identité (champ "title" analysé ES)
|
||||
* → couverture ~50-60% : les tx sans profil géo sont EXCLUES du heatmap
|
||||
* mais comptées dans totalCount / totalVolume
|
||||
* - 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 } from './adapters/SubsquidAdapter';
|
||||
import { resolveGeoByNames } from './adapters/CesiumAdapter';
|
||||
import { fetchTransfers, buildIdentityKeyMap } from './adapters/SubsquidAdapter';
|
||||
import { resolveGeoByKeys } from './adapters/CesiumAdapter';
|
||||
import {
|
||||
getTransactionsForPeriod,
|
||||
computeStats,
|
||||
@@ -22,6 +22,16 @@ import {
|
||||
|
||||
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;
|
||||
@@ -33,13 +43,25 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
||||
const totalCount = rawTransfers.length;
|
||||
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
||||
|
||||
// Résolution géo batch via Cesium+
|
||||
const names = rawTransfers.map((t) => t.fromName).filter(Boolean);
|
||||
// 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 resolveGeoByNames(names);
|
||||
for (const [name, p] of profiles) {
|
||||
geoMap.set(name, { lat: p.lat, lng: p.lng, city: p.city });
|
||||
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);
|
||||
@@ -48,8 +70,10 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
||||
// Seules les transactions avec un profil géo entrent dans le heatmap
|
||||
const geolocated: Transaction[] = [];
|
||||
for (const t of rawTransfers) {
|
||||
const geo = geoMap.get(t.fromName.toLowerCase());
|
||||
if (!geo) continue; // pas de profil → exclu du heatmap
|
||||
const duniterKey = keyMap.get(t.fromId);
|
||||
if (!duniterKey) continue;
|
||||
const geo = geoMap.get(duniterKey);
|
||||
if (!geo) continue;
|
||||
|
||||
geolocated.push({
|
||||
id: t.id,
|
||||
@@ -100,7 +124,7 @@ export async function fetchData(periodDays: number): Promise<DataResult> {
|
||||
return {
|
||||
transactions: geolocated,
|
||||
stats: {
|
||||
totalVolume, // vrai total blockchain
|
||||
totalVolume,
|
||||
transactionCount: totalCount,
|
||||
geoCount: geolocated.length,
|
||||
topCities: base.topCities,
|
||||
|
||||
Reference in New Issue
Block a user