Sources de données réelles Ğ1v2 (vérifiées par introspection) : - SubsquidAdapter : https://squidv2s.syoul.fr/v1/graphql transfers(filter: timestamp >= since, orderBy: TIMESTAMP_DESC) with from.linkedIdentity.name pour jointure géo - CesiumAdapter : https://g1.data.e-is.pro (59 841 profils) recherche batch par nom d'identité (title.keyword) g1.data.duniter.fr hors ligne depuis arrêt Ğ1v1 Schémas Zod mis à jour pour Ğ1v2 : - G1v2KeySchema : SS58 "g1" + 47 chars = 49 chars (mesuré sur données réelles) - SubsquidTransferSchema : id, blockNumber, timestamp ISO, amount BigInt string - parseSubsquidAmount : BigInt string centimes → Ğ1 flottant Activation : VITE_USE_LIVE_API=true dans .env.local Badge "● live Ğ1v2 / ○ mock" ajouté dans l'UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
export interface Transaction {
|
|
id: string;
|
|
timestamp: number; // Unix ms (entier)
|
|
lat: number;
|
|
lng: number;
|
|
amount: number; // Ğ1 (pas en centimes)
|
|
city: string;
|
|
fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars
|
|
toKey: string;
|
|
}
|
|
|
|
// French + European cities where Ğ1 is used
|
|
const CITIES: { name: string; lat: number; lng: number; weight: number }[] = [
|
|
{ name: 'Paris', lat: 48.8566, lng: 2.3522, weight: 12 },
|
|
{ name: 'Lyon', lat: 45.7640, lng: 4.8357, weight: 9 },
|
|
{ name: 'Bordeaux', lat: 44.8378, lng: -0.5792, weight: 8 },
|
|
{ name: 'Toulouse', lat: 43.6047, lng: 1.4442, weight: 8 },
|
|
{ name: 'Montpellier', lat: 43.6108, lng: 3.8767, weight: 7 },
|
|
{ name: 'Nantes', lat: 47.2184, lng: -1.5536, weight: 6 },
|
|
{ name: 'Rennes', lat: 48.1173, lng: -1.6778, weight: 6 },
|
|
{ name: 'Grenoble', lat: 45.1885, lng: 5.7245, weight: 5 },
|
|
{ name: 'Marseille', lat: 43.2965, lng: 5.3698, weight: 7 },
|
|
{ name: 'Strasbourg', lat: 48.5734, lng: 7.7521, weight: 4 },
|
|
{ name: 'Lille', lat: 50.6292, lng: 3.0573, weight: 4 },
|
|
{ name: 'Rouen', lat: 49.4432, lng: 1.0993, weight: 3 },
|
|
{ name: 'Clermont-Ferrand', lat: 45.7772, lng: 3.0870, weight: 4 },
|
|
{ name: 'Tours', lat: 47.3941, lng: 0.6848, weight: 3 },
|
|
{ name: 'Poitiers', lat: 46.5802, lng: 0.3404, weight: 3 },
|
|
{ name: 'Besançon', lat: 47.2378, lng: 6.0241, weight: 3 },
|
|
{ name: 'Caen', lat: 49.1829, lng: -0.3707, weight: 2 },
|
|
{ name: 'Nice', lat: 43.7102, lng: 7.2620, weight: 4 },
|
|
{ name: 'Barcelone', lat: 41.3851, lng: 2.1734, weight: 3 },
|
|
{ name: 'Bruxelles', lat: 50.8503, lng: 4.3517, weight: 3 },
|
|
{ name: 'Genève', lat: 46.2044, lng: 6.1432, weight: 2 },
|
|
{ name: 'Saint-Étienne', lat: 45.4397, lng: 4.3872, weight: 3 },
|
|
{ name: 'Dijon', lat: 47.3220, lng: 5.0415, weight: 3 },
|
|
{ name: 'Angers', lat: 47.4784, lng: -0.5632, weight: 2 },
|
|
];
|
|
|
|
function randomBetween(min: number, max: number): number {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
function weightedRandom<T extends { weight: number }>(items: T[]): T {
|
|
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
|
|
let rand = Math.random() * totalWeight;
|
|
for (const item of items) {
|
|
rand -= item.weight;
|
|
if (rand <= 0) return item;
|
|
}
|
|
return items[items.length - 1];
|
|
}
|
|
|
|
// Génère une clé SS58 Ğ1v2 simulée : préfixe "g1" + 48 chars base58
|
|
function generateKey(): string {
|
|
const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
const suffix = Array.from({ length: 47 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
return 'g1' + suffix;
|
|
}
|
|
|
|
function generateTransactions(count: number, maxAgeMs: number): Transaction[] {
|
|
const now = Date.now();
|
|
const transactions: Transaction[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const city = weightedRandom(CITIES);
|
|
const lat = city.lat + randomBetween(-0.08, 0.08);
|
|
const lng = city.lng + randomBetween(-0.12, 0.12);
|
|
const amount = Math.round(randomBetween(0.5, 150) * 100) / 100;
|
|
|
|
transactions.push({
|
|
id: `tx-${i}-${Math.random().toString(36).slice(2)}`,
|
|
timestamp: Math.floor(now - Math.random() * maxAgeMs),
|
|
lat,
|
|
lng,
|
|
amount,
|
|
city: city.name,
|
|
fromKey: generateKey(),
|
|
toKey: generateKey(),
|
|
});
|
|
}
|
|
|
|
return transactions.sort((a, b) => b.timestamp - a.timestamp);
|
|
}
|
|
|
|
const TRANSACTION_POOL = generateTransactions(2400, 30 * 24 * 60 * 60 * 1000);
|
|
|
|
export function getTransactionsForPeriod(periodDays: number): Transaction[] {
|
|
const cutoff = Date.now() - periodDays * 24 * 60 * 60 * 1000;
|
|
return TRANSACTION_POOL.filter((tx) => tx.timestamp >= cutoff);
|
|
}
|
|
|
|
export function computeStats(transactions: Transaction[]) {
|
|
const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0);
|
|
const transactionCount = transactions.length;
|
|
|
|
const cityVolumes: Record<string, { volume: number; count: number }> = {};
|
|
for (const tx of transactions) {
|
|
if (!cityVolumes[tx.city]) {
|
|
cityVolumes[tx.city] = { volume: 0, count: 0 };
|
|
}
|
|
cityVolumes[tx.city].volume += tx.amount;
|
|
cityVolumes[tx.city].count += 1;
|
|
}
|
|
|
|
const topCities = Object.entries(cityVolumes)
|
|
.sort((a, b) => b[1].volume - a[1].volume)
|
|
.slice(0, 3)
|
|
.map(([name, data]) => ({ name, ...data }));
|
|
|
|
return { totalVolume, transactionCount, topCities };
|
|
}
|
|
|
|
export type { };
|