feat: afficher l'équivalent DU pour le volume total et la moyenne de transaction
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- SubsquidAdapter : fetchCurrentUD() interroge universalDividends (fallback 11.78 Ğ1) - DataService : getCurrentUD() avec cache 1h, inclus dans DataResult - StatsPanel : formatDU() + affichage "≈ X DU" sous le volume total et "≈ X Ğ1 / tx · ≈ Y DU / tx" sous le compteur de transactions - DU actuel Ğ1v2 : 11.78 Ğ1 (bloc 225874) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -17,6 +17,7 @@ export default function App() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
||||||
|
const [currentUD, setCurrentUD] = useState<number>(11.78);
|
||||||
|
|
||||||
const animation = useAnimation(transactions, periodDays);
|
const animation = useAnimation(transactions, periodDays);
|
||||||
|
|
||||||
@@ -32,11 +33,12 @@ export default function App() {
|
|||||||
if (showLoading) setLoading(true);
|
if (showLoading) setLoading(true);
|
||||||
else setRefreshing(true);
|
else setRefreshing(true);
|
||||||
fetchData(periodDays)
|
fetchData(periodDays)
|
||||||
.then(({ transactions, stats, source }) => {
|
.then(({ transactions, stats, source, currentUD }) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setTransactions(transactions);
|
setTransactions(transactions);
|
||||||
setStats(stats);
|
setStats(stats);
|
||||||
setSource(source);
|
setSource(source);
|
||||||
|
setCurrentUD(currentUD);
|
||||||
setLastUpdate(new Date());
|
setLastUpdate(new Date());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -68,6 +70,7 @@ export default function App() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
periodDays={periodDays}
|
periodDays={periodDays}
|
||||||
source={source}
|
source={source}
|
||||||
|
currentUD={currentUD}
|
||||||
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
|
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface StatsPanelProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
periodDays: number;
|
periodDays: number;
|
||||||
source: 'live' | 'mock';
|
source: 'live' | 'mock';
|
||||||
|
currentUD: number;
|
||||||
animationLabel?: string;
|
animationLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,14 @@ function StatCard({ label, value, sub, delta }: { label: string; value: string;
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays, source, animationLabel }: StatsPanelProps) {
|
function formatDU(g1: number, ud: number): string {
|
||||||
|
const du = g1 / ud;
|
||||||
|
if (du < 10) return `≈ ${du.toFixed(2)} DU`;
|
||||||
|
if (du < 100) return `≈ ${du.toFixed(1)} DU`;
|
||||||
|
return `≈ ${Math.round(du).toLocaleString('fr-FR')} DU`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel }: StatsPanelProps) {
|
||||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||||
const prevStats = useRef<PeriodStats | null>(null);
|
const prevStats = useRef<PeriodStats | null>(null);
|
||||||
|
|
||||||
@@ -89,12 +97,16 @@ export function StatsPanel({ stats, loading, periodDays, source, animationLabel
|
|||||||
<StatCard
|
<StatCard
|
||||||
label="Volume total"
|
label="Volume total"
|
||||||
value={`${stats.totalVolume.toLocaleString('fr-FR', { maximumFractionDigits: 2 })} Ğ1`}
|
value={`${stats.totalVolume.toLocaleString('fr-FR', { maximumFractionDigits: 2 })} Ğ1`}
|
||||||
|
sub={formatDU(stats.totalVolume, currentUD)}
|
||||||
delta={prevVolume !== null ? (stats.totalVolume > prevVolume ? 'up' : stats.totalVolume < prevVolume ? 'down' : null) : null}
|
delta={prevVolume !== null ? (stats.totalVolume > prevVolume ? 'up' : stats.totalVolume < prevVolume ? 'down' : null) : null}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Transactions"
|
label="Transactions"
|
||||||
value={stats.transactionCount.toLocaleString('fr-FR')}
|
value={stats.transactionCount.toLocaleString('fr-FR')}
|
||||||
sub={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
sub={(() => {
|
||||||
|
const avg = stats.totalVolume / (stats.transactionCount || 1);
|
||||||
|
return `≈ ${avg.toFixed(2)} Ğ1 / tx · ${formatDU(avg, currentUD)} / tx`;
|
||||||
|
})()}
|
||||||
delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null}
|
delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null}
|
||||||
/>
|
/>
|
||||||
{/* Couverture géo — uniquement en mode live */}
|
{/* Couverture géo — uniquement en mode live */}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchTransfers, buildIdentityKeyMap } from './adapters/SubsquidAdapter';
|
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD } from './adapters/SubsquidAdapter';
|
||||||
import { resolveGeoByKeys } from './adapters/CesiumAdapter';
|
import { resolveGeoByKeys } from './adapters/CesiumAdapter';
|
||||||
import {
|
import {
|
||||||
getTransactionsForPeriod,
|
getTransactionsForPeriod,
|
||||||
@@ -22,6 +22,16 @@ import {
|
|||||||
|
|
||||||
const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true';
|
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<number> {
|
||||||
|
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
|
// Cache de la carte identité SS58→DuniterKey, valide 10 minutes
|
||||||
let keyMapCache: { map: Map<string, string>; expiresAt: number } | null = null;
|
let keyMapCache: { map: Map<string, string>; expiresAt: number } | null = null;
|
||||||
|
|
||||||
@@ -106,6 +116,7 @@ export interface DataResult {
|
|||||||
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
||||||
stats: PeriodStats;
|
stats: PeriodStats;
|
||||||
source: 'live' | 'mock';
|
source: 'live' | 'mock';
|
||||||
|
currentUD: number; // valeur du DU courant en Ğ1
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchData(periodDays: number): Promise<DataResult> {
|
export async function fetchData(periodDays: number): Promise<DataResult> {
|
||||||
@@ -117,10 +128,14 @@ export async function fetchData(periodDays: number): Promise<DataResult> {
|
|||||||
transactions,
|
transactions,
|
||||||
stats: { ...base, geoCount: transactions.length },
|
stats: { ...base, geoCount: transactions.length },
|
||||||
source: 'mock',
|
source: 'mock',
|
||||||
|
currentUD: 11.78,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { geolocated, totalCount, totalVolume } = await fetchLiveTransactions(periodDays);
|
const [{ geolocated, totalCount, totalVolume }, currentUD] = await Promise.all([
|
||||||
|
fetchLiveTransactions(periodDays),
|
||||||
|
getCurrentUD(),
|
||||||
|
]);
|
||||||
const base = computeStats(geolocated);
|
const base = computeStats(geolocated);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -132,5 +147,6 @@ export async function fetchData(periodDays: number): Promise<DataResult> {
|
|||||||
topCities: base.topCities,
|
topCities: base.topCities,
|
||||||
},
|
},
|
||||||
source: 'live',
|
source: 'live',
|
||||||
|
currentUD,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,27 @@ export async function buildIdentityKeyMap(): Promise<Map<string, string>> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retourne la valeur du DU courant en Ğ1 (ex : 11.78). Fallback hardcodé si indisponible. */
|
||||||
|
export async function fetchCurrentUD(): Promise<number> {
|
||||||
|
const UD_FALLBACK = 11.78; // valeur au bloc 225874 — mis à jour si la requête échoue
|
||||||
|
try {
|
||||||
|
const response = await fetch(SUBSQUID_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `{ universalDividends(orderBy: BLOCK_NUMBER_DESC, first: 1) { nodes { amount } } }`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) return UD_FALLBACK;
|
||||||
|
const raw = await response.json();
|
||||||
|
const amountStr: string | undefined = raw?.data?.universalDividends?.nodes?.[0]?.amount;
|
||||||
|
if (!amountStr) return UD_FALLBACK;
|
||||||
|
return parseInt(amountStr, 10) / 100;
|
||||||
|
} catch {
|
||||||
|
return UD_FALLBACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface FetchTransfersResult {
|
export interface FetchTransfersResult {
|
||||||
transfers: RawTransfer[];
|
transfers: RawTransfer[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user