Compare commits
7 Commits
c6cb990b40
...
94474fc007
| Author | SHA1 | Date | |
|---|---|---|---|
| 94474fc007 | |||
| d99ad3707d | |||
| 55d2b50cd3 | |||
| 8d9a9a3c07 | |||
| a8792641ab | |||
| a6fc4a534f | |||
| 2674b3891b |
+13
-3
@@ -11,6 +11,8 @@ export default function App() {
|
|||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
const [stats, setStats] = useState<PeriodStats | null>(null);
|
const [stats, setStats] = useState<PeriodStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,13 +20,19 @@ export default function App() {
|
|||||||
|
|
||||||
const load = (showLoading: boolean) => {
|
const load = (showLoading: boolean) => {
|
||||||
if (showLoading) setLoading(true);
|
if (showLoading) setLoading(true);
|
||||||
fetchData(periodDays).then(({ transactions, stats, source }) => {
|
else setRefreshing(true);
|
||||||
|
fetchData(periodDays)
|
||||||
|
.then(({ transactions, stats, source }) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setTransactions(transactions);
|
setTransactions(transactions);
|
||||||
setStats(stats);
|
setStats(stats);
|
||||||
setSource(source);
|
setSource(source);
|
||||||
setLoading(false);
|
setLastUpdate(new Date());
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.warn('Ğ1Flux refresh error:', err))
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) { setLoading(false); setRefreshing(false); }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +67,9 @@ export default function App() {
|
|||||||
? 'bg-emerald-950/80 border-emerald-700 text-emerald-400'
|
? 'bg-emerald-950/80 border-emerald-700 text-emerald-400'
|
||||||
: 'bg-[#0a0b0f]/80 border-[#2e2f3a] text-[#4b5563]'
|
: 'bg-[#0a0b0f]/80 border-[#2e2f3a] text-[#4b5563]'
|
||||||
}`}>
|
}`}>
|
||||||
{source === 'live' ? '● live Ğ1v2' : '○ mock'}
|
{source === 'live'
|
||||||
|
? <>{refreshing ? <span className="animate-spin inline-block">↻</span> : '●'} live Ğ1v2{lastUpdate && <span className="ml-1 opacity-60">{lastUpdate.toLocaleTimeString('fr-FR')}</span>}</>
|
||||||
|
: '○ mock'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
import type { PeriodStats } from '../services/DataService';
|
import type { PeriodStats } from '../services/DataService';
|
||||||
|
|
||||||
interface StatsPanelProps {
|
interface StatsPanelProps {
|
||||||
@@ -9,11 +10,15 @@ interface StatsPanelProps {
|
|||||||
|
|
||||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||||
|
|
||||||
function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
function StatCard({ label, value, sub, delta }: { label: string; value: string; sub?: string; delta?: 'up' | 'down' | null }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-4 space-y-1">
|
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-4 space-y-1">
|
||||||
<p className="text-[#4b5563] text-xs uppercase tracking-widest">{label}</p>
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">{label}</p>
|
||||||
<p className="text-[#d4a843] text-2xl font-bold tabular-nums">{value}</p>
|
<p className="text-[#d4a843] text-2xl font-bold tabular-nums">
|
||||||
|
{value}
|
||||||
|
{delta === 'up' && <span className="text-emerald-400 text-sm ml-1.5">↑</span>}
|
||||||
|
{delta === 'down' && <span className="text-red-400 text-sm ml-1.5">↓</span>}
|
||||||
|
</p>
|
||||||
{sub && <p className="text-[#6b7280] text-xs">{sub}</p>}
|
{sub && <p className="text-[#6b7280] text-xs">{sub}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -21,6 +26,26 @@ function StatCard({ label, value, sub }: { label: string; value: string; sub?: s
|
|||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelProps) {
|
export function StatsPanel({ stats, loading, periodDays, source }: 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);
|
||||||
|
|
||||||
|
// Calcule le delta d'une valeur par rapport au refresh précédent
|
||||||
|
function delta(current: number, prevMap: Map<string, number>, key: string) {
|
||||||
|
const prev = prevMap.get(key);
|
||||||
|
if (prev === undefined) return null;
|
||||||
|
if (current > prev) return <span className="text-emerald-400 text-xs ml-1">↑</span>;
|
||||||
|
if (current < prev) return <span className="text-red-400 text-xs ml-1">↓</span>;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construit une map volume précédent par ville
|
||||||
|
const prevCityVolume = new Map(
|
||||||
|
(prevStats.current?.topCities ?? []).map((c) => [c.name, c.volume])
|
||||||
|
);
|
||||||
|
const prevVolume = prevStats.current?.totalVolume ?? null;
|
||||||
|
const prevTxCount = prevStats.current?.transactionCount ?? null;
|
||||||
|
|
||||||
|
// Mémorise les stats après le rendu
|
||||||
|
if (stats && !loading) prevStats.current = stats;
|
||||||
const geoPct = stats && stats.transactionCount > 0
|
const geoPct = stats && stats.transactionCount > 0
|
||||||
? Math.round((stats.geoCount / stats.transactionCount) * 100)
|
? Math.round((stats.geoCount / stats.transactionCount) * 100)
|
||||||
: null;
|
: null;
|
||||||
@@ -55,11 +80,13 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro
|
|||||||
<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`}
|
||||||
|
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={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
||||||
|
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 */}
|
||||||
{source === 'live' && geoPct !== null && (
|
{source === 'live' && geoPct !== null && (
|
||||||
@@ -89,16 +116,26 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro
|
|||||||
{stats.topCities.map((city, i) => (
|
{stats.topCities.map((city, i) => (
|
||||||
<div
|
<div
|
||||||
key={city.name}
|
key={city.name}
|
||||||
className="bg-[#0f1016] border border-[#2e2f3a] rounded-lg px-3 py-2.5 flex items-center gap-3"
|
className="bg-[#0f1016] border border-[#2e2f3a] rounded-lg px-3 py-2.5 flex gap-2.5"
|
||||||
>
|
>
|
||||||
<span className="text-base">{MEDALS[i]}</span>
|
<span className="text-base shrink-0 mt-0.5">{MEDALS[i]}</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-white text-sm font-medium truncate">{city.name}</p>
|
<p className="text-white text-xs font-medium leading-snug">{city.name}</p>
|
||||||
<p className="text-[#6b7280] text-xs">{city.count} tx</p>
|
<div className="flex items-center justify-between mt-1">
|
||||||
</div>
|
<span className="flex items-center gap-1.5 text-[#6b7280] text-xs">
|
||||||
<span className="text-[#d4a843] text-sm font-mono shrink-0">
|
{city.countryCode && (
|
||||||
{city.volume.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} Ğ1
|
<span className="text-[10px] font-bold bg-[#1e1f2a] text-[#6b7280] rounded px-1 py-0.5 leading-none">
|
||||||
|
{city.countryCode}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
{city.count} tx
|
||||||
|
</span>
|
||||||
|
<span className="text-[#d4a843] text-xs font-mono flex items-center gap-1">
|
||||||
|
{city.volume.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} Ğ1
|
||||||
|
{delta(city.volume, prevCityVolume, city.name)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface Transaction {
|
|||||||
lng: number;
|
lng: number;
|
||||||
amount: number; // Ğ1 (pas en centimes)
|
amount: number; // Ğ1 (pas en centimes)
|
||||||
city: string;
|
city: string;
|
||||||
|
countryCode: string; // ISO 3166-1 alpha-2, ex: "FR"
|
||||||
fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars
|
fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars
|
||||||
toKey: string;
|
toKey: string;
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,7 @@ function generateTransactions(count: number, maxAgeMs: number): Transaction[] {
|
|||||||
lng,
|
lng,
|
||||||
amount,
|
amount,
|
||||||
city: city.name,
|
city: city.name,
|
||||||
|
countryCode: 'FR',
|
||||||
fromKey: generateKey(),
|
fromKey: generateKey(),
|
||||||
toKey: generateKey(),
|
toKey: generateKey(),
|
||||||
});
|
});
|
||||||
@@ -94,10 +96,10 @@ export function computeStats(transactions: Transaction[]) {
|
|||||||
const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0);
|
const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0);
|
||||||
const transactionCount = transactions.length;
|
const transactionCount = transactions.length;
|
||||||
|
|
||||||
const cityVolumes: Record<string, { volume: number; count: number }> = {};
|
const cityVolumes: Record<string, { volume: number; count: number; countryCode: string }> = {};
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
if (!cityVolumes[tx.city]) {
|
if (!cityVolumes[tx.city]) {
|
||||||
cityVolumes[tx.city] = { volume: 0, count: 0 };
|
cityVolumes[tx.city] = { volume: 0, count: 0, countryCode: tx.countryCode ?? '' };
|
||||||
}
|
}
|
||||||
cityVolumes[tx.city].volume += tx.amount;
|
cityVolumes[tx.city].volume += tx.amount;
|
||||||
cityVolumes[tx.city].count += 1;
|
cityVolumes[tx.city].count += 1;
|
||||||
|
|||||||
@@ -37,10 +37,9 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
}> {
|
}> {
|
||||||
const rawTransfers = await fetchTransfers(periodDays);
|
const { transfers: rawTransfers, totalCount } = await fetchTransfers(periodDays);
|
||||||
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 };
|
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 };
|
||||||
|
|
||||||
const totalCount = rawTransfers.length;
|
|
||||||
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
||||||
|
|
||||||
// Carte SS58 courant → clé Duniter (= _id Cesium+)
|
// Carte SS58 courant → clé Duniter (= _id Cesium+)
|
||||||
@@ -57,11 +56,11 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
// Résolution géo par clé Duniter (_id Cesium+)
|
// Résolution géo par clé Duniter (_id Cesium+)
|
||||||
let geoMap = new Map<string, { lat: number; lng: number; city: string }>();
|
let geoMap = new Map<string, { lat: number; lng: number; city: string; countryCode: string }>();
|
||||||
try {
|
try {
|
||||||
const profiles = await resolveGeoByKeys(duniterKeys);
|
const profiles = await resolveGeoByKeys(duniterKeys);
|
||||||
for (const [key, p] of profiles) {
|
for (const [key, p] of profiles) {
|
||||||
geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city });
|
geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city, countryCode: p.countryCode });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Cesium+ indisponible :', err);
|
console.warn('Cesium+ indisponible :', err);
|
||||||
@@ -82,6 +81,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
lng: geo.lng,
|
lng: geo.lng,
|
||||||
amount: t.amount,
|
amount: t.amount,
|
||||||
city: geo.city,
|
city: geo.city,
|
||||||
|
countryCode: geo.countryCode,
|
||||||
fromKey: t.fromId,
|
fromKey: t.fromId,
|
||||||
toKey: t.toId,
|
toKey: t.toId,
|
||||||
});
|
});
|
||||||
@@ -97,7 +97,7 @@ export interface PeriodStats {
|
|||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
transactionCount: number; // total blockchain (y compris non-géolocalisés)
|
transactionCount: number; // total blockchain (y compris non-géolocalisés)
|
||||||
geoCount: number; // transactions visibles sur la carte
|
geoCount: number; // transactions visibles sur la carte
|
||||||
topCities: { name: string; volume: number; count: number }[];
|
topCities: { name: string; volume: number; count: number; countryCode: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataResult {
|
export interface DataResult {
|
||||||
|
|||||||
@@ -18,10 +18,67 @@ export const CESIUM_ENDPOINT = 'https://g1.data.e-is.pro';
|
|||||||
export interface GeoProfile {
|
export interface GeoProfile {
|
||||||
name: string; // nom d'identité Ğ1 (ex: "Anikka")
|
name: string; // nom d'identité Ğ1 (ex: "Anikka")
|
||||||
city: string;
|
city: string;
|
||||||
|
countryCode: string; // ISO 3166-1 alpha-2, ex: "FR"
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Détection de pays par bounding box (pays présents dans la communauté Ğ1)
|
||||||
|
const COUNTRY_BOXES: { code: string; latMin: number; latMax: number; lngMin: number; lngMax: number }[] = [
|
||||||
|
// Petits pays d'abord : leurs bounding boxes chevauchent celle de la France
|
||||||
|
{ code: 'LU', latMin: 49.4, latMax: 50.2, lngMin: 5.7, lngMax: 6.5 },
|
||||||
|
{ code: 'BE', latMin: 49.5, latMax: 51.5, lngMin: 2.5, lngMax: 6.4 },
|
||||||
|
{ code: 'CH', latMin: 45.8, latMax: 47.8, lngMin: 5.9, lngMax: 10.5 },
|
||||||
|
{ code: 'NL', latMin: 50.7, latMax: 53.6, lngMin: 3.3, lngMax: 7.2 },
|
||||||
|
{ code: 'DE', latMin: 47.3, latMax: 55.1, lngMin: 6.0, lngMax: 15.0 },
|
||||||
|
{ code: 'FR', latMin: 41.3, latMax: 51.1, lngMin: -5.1, lngMax: 9.6 },
|
||||||
|
{ code: 'ES', latMin: 35.9, latMax: 43.8, lngMin: -9.3, lngMax: 4.3 },
|
||||||
|
{ code: 'PT', latMin: 36.8, latMax: 42.2, lngMin: -9.5, lngMax: -6.2 },
|
||||||
|
{ code: 'IT', latMin: 36.6, latMax: 47.1, lngMin: 6.6, lngMax: 18.5 },
|
||||||
|
{ code: 'GB', latMin: 49.9, latMax: 60.9, lngMin: -8.2, lngMax: 1.8 },
|
||||||
|
{ code: 'MA', latMin: 27.6, latMax: 35.9, lngMin: -13.2, lngMax: -1.0 },
|
||||||
|
{ code: 'TN', latMin: 30.2, latMax: 37.5, lngMin: 7.5, lngMax: 11.6 },
|
||||||
|
{ code: 'SN', latMin: 12.3, latMax: 16.7, lngMin: -17.5, lngMax: -11.4 },
|
||||||
|
{ code: 'CA', latMin: 41.7, latMax: 83.1, lngMin: -141.0,lngMax: -52.6 },
|
||||||
|
{ code: 'BR', latMin: -33.7, latMax: 5.3, lngMin: -73.9, lngMax: -34.8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function latLngToCountryCode(lat: number, lng: number): string {
|
||||||
|
// France métropolitaine en premier (cas le plus fréquent)
|
||||||
|
for (const b of COUNTRY_BOXES) {
|
||||||
|
if (lat >= b.latMin && lat <= b.latMax && lng >= b.lngMin && lng <= b.lngMax) return b.code;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nettoie le nom de ville Cesium+ : retire le code postal ("Paris, 75001" → "Paris") */
|
||||||
|
export function cleanCityName(city: string): string {
|
||||||
|
return city.split(',')[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noms de pays en français/anglais → code ISO (Cesium+ utilise le français)
|
||||||
|
const COUNTRY_NAME_TO_CODE: Record<string, string> = {
|
||||||
|
'france': 'FR', 'belgique': 'BE', 'belgium': 'BE',
|
||||||
|
'suisse': 'CH', 'switzerland': 'CH', 'schweiz': 'CH',
|
||||||
|
'luxembourg': 'LU', 'allemagne': 'DE', 'germany': 'DE',
|
||||||
|
'espagne': 'ES', 'spain': 'ES', 'portugal': 'PT',
|
||||||
|
'italie': 'IT', 'italy': 'IT', 'pays-bas': 'NL',
|
||||||
|
'netherlands': 'NL', 'royaume-uni': 'GB', 'united kingdom': 'GB',
|
||||||
|
'maroc': 'MA', 'morocco': 'MA', 'tunisie': 'TN', 'tunisia': 'TN',
|
||||||
|
'sénégal': 'SN', 'senegal': 'SN', 'canada': 'CA', 'brésil': 'BR', 'brazil': 'BR',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Extrait le pays depuis le champ city Cesium+ (ex: "Heusy, 4800, Belgique" → "BE") */
|
||||||
|
function countryCodeFromCity(city: string): string {
|
||||||
|
const parts = city.split(',');
|
||||||
|
for (let i = parts.length - 1; i >= 0; i--) {
|
||||||
|
const token = parts[i].trim().toLowerCase();
|
||||||
|
const code = COUNTRY_NAME_TO_CODE[token];
|
||||||
|
if (code) return code;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// geoPoint accepte n'importe quel type — Cesium+ utilise plusieurs formats ES geo_point
|
// geoPoint accepte n'importe quel type — Cesium+ utilise plusieurs formats ES geo_point
|
||||||
const HitSchema = z.object({
|
const HitSchema = z.object({
|
||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
@@ -94,9 +151,11 @@ export async function resolveGeoByKeys(
|
|||||||
const src = hit._source;
|
const src = hit._source;
|
||||||
const geo = parseGeoPoint(src.geoPoint);
|
const geo = parseGeoPoint(src.geoPoint);
|
||||||
if (!geo) continue;
|
if (!geo) continue;
|
||||||
|
const city = src.city ?? 'Inconnue';
|
||||||
result.set(hit._id, {
|
result.set(hit._id, {
|
||||||
name: src.title ?? '',
|
name: src.title ?? '',
|
||||||
city: src.city ?? 'Inconnue',
|
city,
|
||||||
|
countryCode: countryCodeFromCity(city) || latLngToCountryCode(geo.lat, geo.lng),
|
||||||
lat: geo.lat,
|
lat: geo.lat,
|
||||||
lng: geo.lng,
|
lng: geo.lng,
|
||||||
});
|
});
|
||||||
@@ -152,9 +211,11 @@ export async function resolveGeoByNames(
|
|||||||
const src = hit._source;
|
const src = hit._source;
|
||||||
const geo = parseGeoPoint(src.geoPoint);
|
const geo = parseGeoPoint(src.geoPoint);
|
||||||
if (geo && src.title) {
|
if (geo && src.title) {
|
||||||
|
const city = src.city ?? 'Inconnue';
|
||||||
result.set(src.title.toLowerCase(), {
|
result.set(src.title.toLowerCase(), {
|
||||||
name: src.title,
|
name: src.title,
|
||||||
city: src.city ?? 'Inconnue',
|
city,
|
||||||
|
countryCode: countryCodeFromCity(city) || latLngToCountryCode(geo.lat, geo.lng),
|
||||||
lat: geo.lat,
|
lat: geo.lat,
|
||||||
lng: geo.lng,
|
lng: geo.lng,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const SubsquidTransferNodeSchema = z.object({
|
|||||||
const SubsquidResponseSchema = z.object({
|
const SubsquidResponseSchema = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
transfers: z.object({
|
transfers: z.object({
|
||||||
|
totalCount: z.number().int(),
|
||||||
nodes: z.array(SubsquidTransferNodeSchema),
|
nodes: z.array(SubsquidTransferNodeSchema),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -62,6 +63,7 @@ const TRANSFERS_QUERY = `
|
|||||||
first: $limit
|
first: $limit
|
||||||
filter: { timestamp: { greaterThanOrEqualTo: $since } }
|
filter: { timestamp: { greaterThanOrEqualTo: $since } }
|
||||||
) {
|
) {
|
||||||
|
totalCount
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
blockNumber
|
blockNumber
|
||||||
@@ -151,10 +153,15 @@ export async function buildIdentityKeyMap(): Promise<Map<string, string>> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FetchTransfersResult {
|
||||||
|
transfers: RawTransfer[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchTransfers(
|
export async function fetchTransfers(
|
||||||
periodDays: number,
|
periodDays: number,
|
||||||
limit = 2000
|
limit = 2000
|
||||||
): Promise<RawTransfer[]> {
|
): Promise<FetchTransfersResult> {
|
||||||
const since = new Date(
|
const since = new Date(
|
||||||
Date.now() - periodDays * 24 * 60 * 60 * 1000
|
Date.now() - periodDays * 24 * 60 * 60 * 1000
|
||||||
).toISOString();
|
).toISOString();
|
||||||
@@ -180,12 +187,15 @@ export async function fetchTransfers(
|
|||||||
|
|
||||||
const parsed = SubsquidResponseSchema.parse(raw);
|
const parsed = SubsquidResponseSchema.parse(raw);
|
||||||
|
|
||||||
return parsed.data.transfers.nodes.map((node) => ({
|
return {
|
||||||
|
totalCount: parsed.data.transfers.totalCount,
|
||||||
|
transfers: parsed.data.transfers.nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
timestamp: new Date(node.timestamp).getTime(),
|
timestamp: new Date(node.timestamp).getTime(),
|
||||||
amount: parseInt(node.amount, 10) / 100,
|
amount: parseInt(node.amount, 10) / 100,
|
||||||
fromId: node.fromId ?? '',
|
fromId: node.fromId ?? '',
|
||||||
toId: node.toId ?? '',
|
toId: node.toId ?? '',
|
||||||
fromName: node.from?.linkedIdentity?.name ?? '',
|
fromName: node.from?.linkedIdentity?.name ?? '',
|
||||||
}));
|
})),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user