Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,15 @@ export default function Home() {

{/* Core Stats Row */}
<div className="grid grid-cols-3 gap-4 border-t border-b border-border/40 py-6">
{/* Pool Value */}
<div className="text-center lg:text-left">
{renderStat(formatCompactUsdc(stats?.totalDeposits))}
<span className="text-[10px] font-mono text-slate-500 uppercase font-bold tracking-wider block mt-1">
USDC POOL VALUE
</span>
</div>

{/* Invoices Funded */}
<div className="text-center lg:text-left">
{renderStat(
stats
Expand All @@ -119,6 +122,8 @@ export default function Home() {
INVOICES FUNDED
</span>
</div>

{/* Yield Distributed */}
<div className="text-center lg:text-left">
{renderStat(formatCompactUsdc(stats?.totalYieldDistributed))}
<span className="text-[10px] font-mono text-slate-500 uppercase font-bold tracking-wider block mt-1">
Expand All @@ -128,7 +133,7 @@ export default function Home() {
</div>

<div className="text-xs font-mono text-slate-400 border-l-2 border-primary pl-3">
&quot;From invoice to USDC in minutes. Not weeks.&quot;
"From invoice to USDC in minutes. Not weeks."
</div>
</div>

Expand Down Expand Up @@ -270,7 +275,15 @@ export default function Home() {
</p>
<div className="bg-[#080c10] border border-border/40 p-2.5 rounded text-[10px] font-mono flex justify-between text-slate-500">
<span>UTILIZATION RATE:</span>
<span className="text-white font-bold">78.5% capacity</span>
{isStatsLoading ? (
<span className="text-white font-bold animate-pulse">--.-%</span>
) : statsError ? (
<span className="text-white font-bold">--.-%</span>
) : (
<span className="text-white font-bold">
{(stats?.utilizationRateBps || 0) / 100}% capacity
</span>
)}
</div>
</div>
</div>
Expand Down
80 changes: 80 additions & 0 deletions apps/web/components/shared/TopStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,86 @@ const tickerItems: TickerItem[] = [
];

export function TopStatusBar() {
const { events: rawEvents, isLoading, isError } = useRecentEvents(20);
const [tickerItems, setTickerItems] = useState<TickerItem[]>([]);

// Format event for display in the ticker
const formatEventForTicker = (event: EventLog): TickerItem => {
// Extract amount from event data (assuming it's in USDC)
let amount = '0 USDC';
if (event.data && event.data.funded_amount) {
const amountInUSDC = Number(event.data.funded_amount) / 1000000; // Convert from stroops to USDC
amount = `${Math.round(amountInUSDC).toLocaleString()} USDC`;
} else if (event.data && event.data.face_value) {
const amountInUSDC = Number(event.data.face_value) / 1000000; // Convert from stroops to USDC
amount = `${Math.round(amountInUSDC).toLocaleString()} USDC`;
}

// Extract discount from event data (in basis points)
let discount = '0.0%';
if (event.data && event.data.discount_bps) {
const discountPercent = Number(event.data.discount_bps) / 100;
discount = `${discountPercent.toFixed(1)}%`;
}

// Calculate time ago
const now = Math.floor(Date.now() / 1000);
const diff = now - event.ledger_closed_at;
let time = '';
if (diff < 60) time = 'just now';
else if (diff < 3600) time = `${Math.floor(diff / 60)}m ago`;
else if (diff < 86400) time = `${Math.floor(diff / 3600)}h ago`;
else time = `${Math.floor(diff / 86400)}d ago`;

// Determine country/flag based on issuer or buyer (simplified hash-based)
const flagMap: Record<string, string> = {
'0': '🇳🇬', '1': '🇰🇪', '2': '🇬🇭', '3': '🇸🇳', '4': '🇺🇬',
'5': '🇨🇮', '6': '🇹🇬', '7': '🇧🇯', '8': '🇸🇱', '9': '🇱🇷'
};
const hash = Array.from(event.id.toString()).reduce((acc, char) => acc + char.charCodeAt(0), 0);
const flag = flagMap[hash % 10] || '🌍';

// Generate SME name based on event data
let sme = 'Unknown SME';
if (event.data.buyer) {
sme = `${event.data.buyer.slice(0, 8)}...`;
} else if (event.data.issuer) {
sme = `${event.data.issuer.slice(0, 8)}...`;
}

return {
id: event.id.toString(),
sme,
amount,
discount,
time,
country: flag,
};
};

useEffect(() => {
if (rawEvents && rawEvents.length > 0) {
// Convert events to ticker items
const items = rawEvents.map(formatEventForTicker);
setTickerItems(items);
}
}, [rawEvents]);

// If no real data, show some placeholder items to maintain the ticker effect
useEffect(() => {
if (!rawEvents || rawEvents.length === 0) {
// Show placeholder items when no real data is available
const placeholderItems: TickerItem[] = [
{ id: '1', sme: 'Awaiting...', amount: '0 USDC', discount: '0.0%', time: 'live', country: '🌍' },
{ id: '2', sme: 'Awaiting...', amount: '0 USDC', discount: '0.0%', time: 'live', country: '🌍' },
{ id: '3', sme: 'Awaiting...', amount: '0 USDC', discount: '0.0%', time: 'live', country: '🌍' },
{ id: '4', sme: 'Awaiting...', amount: '0 USDC', discount: '0.0%', time: 'live', country: '🌍' },
{ id: '5', sme: 'Awaiting...', amount: '0 USDC', discount: '0.0%', time: 'live', country: '🌍' },
];
setTickerItems(placeholderItems);
}
}, [rawEvents]);

return (
<div className="w-full bg-[#080c10] border-b border-border py-1.5 px-4 overflow-hidden relative z-40 flex items-center justify-between gap-4">
{/* Network indicator */}
Expand Down
20 changes: 17 additions & 3 deletions indexer/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
type APIHandler struct {
cfg *config.Config
serverKP *keypair.Full
statsMu sync.Mutex
statsMu sync.RWMutex
statsData *db.ProtocolStats
statsCached time.Time
}
Expand Down Expand Up @@ -759,23 +759,37 @@ func (h *APIHandler) HandleGetInvoices(w http.ResponseWriter, r *http.Request) {

// GET /stats
func (h *APIHandler) HandleGetStats(w http.ResponseWriter, r *http.Request) {
// Try to read the cache with a read lock
h.statsMu.RLock()
if h.statsData != nil && time.Since(h.statsCached) < 30*time.Second {
data := h.statsData
h.statsMu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
return
}
h.statsMu.RUnlock()

// Cache is missing or expired, we need to update it.
// Take a write lock (but first we must release the read lock, which we did above).
h.statsMu.Lock()
// Double-check the cache after acquiring the write lock.
if h.statsData != nil && time.Since(h.statsCached) < 30*time.Second {
data := h.statsData
h.statsMu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
return
}
h.statsMu.Unlock()

// Cache is still invalid, fetch from DB and update.
stats, err := db.GetProtocolStats(r.Context())
if err != nil {
h.statsMu.Unlock()
http.Error(w, fmt.Sprintf("failed to retrieve protocol stats: %s", err.Error()), http.StatusInternalServerError)
return
}

h.statsMu.Lock()
h.statsData = stats
h.statsCached = time.Now()
h.statsMu.Unlock()
Expand Down