3aa3933b4c
ci/woodpecker/push/woodpecker Pipeline was successful
Les timestamps du pool étaient figés au moment du chargement du module. On calcule le drift entre l'heure de génération et l'heure courante, et on le réapplique à chaque appel à getTransactionsForPeriod. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
export interface Transaction {
|
|
id: string;
|
|
timestamp: number; // Unix ms (entier)
|
|
lat: number;
|
|
lng: number;
|
|
amount: number; // Ğ1 (pas en centimes)
|
|
city: string;
|
|
countryCode: string; // ISO 3166-1 alpha-2, ex: "FR"
|
|
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,
|
|
countryCode: 'FR',
|
|
fromKey: generateKey(),
|
|
toKey: generateKey(),
|
|
});
|
|
}
|
|
|
|
return transactions.sort((a, b) => b.timestamp - a.timestamp);
|
|
}
|
|
|
|
const POOL_GENERATED_AT = Date.now();
|
|
const TRANSACTION_POOL = generateTransactions(2400, 30 * 24 * 60 * 60 * 1000);
|
|
|
|
export function getTransactionsForPeriod(periodDays: number): Transaction[] {
|
|
const drift = Date.now() - POOL_GENERATED_AT;
|
|
const cutoff = Date.now() - periodDays * 24 * 60 * 60 * 1000;
|
|
return TRANSACTION_POOL
|
|
.map((tx) => ({ ...tx, timestamp: tx.timestamp + drift }))
|
|
.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; countryCode: string }> = {};
|
|
for (const tx of transactions) {
|
|
if (!cityVolumes[tx.city]) {
|
|
cityVolumes[tx.city] = { volume: 0, count: 0, countryCode: tx.countryCode ?? '' };
|
|
}
|
|
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 { };
|