Skip to content

Commit 12950a5

Browse files
committed
fix bug
1 parent c8606b1 commit 12950a5

File tree

5 files changed

+132
-9
lines changed

5 files changed

+132
-9
lines changed

backend/env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ GITLAB_CLIENT_SECRET=your-gitlab-client-secret
124124

125125
# OpenAI Configuration
126126
OPENAI_API_KEY=your-openai-api-key-here
127+
# Optional CoinGecko demo key for live treasury token prices
128+
COINGECKO_DEMO_API_KEY=
127129
# For production recommended gpt-5, for cheap testing use gpt-5-mini (don't use gpt-5-nano: it tends to enter infinite loop with Web search)
128130
OPENAI_MODEL=gpt-5
129131
# batch (for production, not yet debugged) or nonbatch (useful for debugging)

backend/src/routes/global.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import express from 'express';
22
import { GlobalDataService } from '../services/GlobalDataService.js';
3+
import { TokenPriceService } from '../services/TokenPriceService.js';
34

45
const router = express.Router();
56

@@ -76,4 +77,37 @@ router.post('/refresh-gdp', async (req, res) => {
7677
}
7778
});
7879

80+
/**
81+
* GET /api/global/token-prices
82+
* Get live USD token prices
83+
*/
84+
router.get('/token-prices', async (req, res) => {
85+
try {
86+
const rawSymbols = typeof req.query.symbols === 'string' ? req.query.symbols : '';
87+
const symbols = rawSymbols
88+
.split(',')
89+
.map(symbol => symbol.trim().toUpperCase())
90+
.filter(Boolean);
91+
92+
const quotes = await TokenPriceService.getUsdQuotes(symbols);
93+
94+
return res.json({
95+
success: true,
96+
data: {
97+
quotes,
98+
source: 'coingecko',
99+
requestedSymbols: symbols,
100+
supportedSymbols: TokenPriceService.getSupportedSymbols()
101+
}
102+
});
103+
} catch (error) {
104+
console.error('Error getting token prices:', error);
105+
return res.status(500).json({
106+
success: false,
107+
message: 'Failed to retrieve token prices',
108+
error: process.env.NODE_ENV === 'development' ? (error as Error).message : 'Something went wrong'
109+
});
110+
}
111+
});
112+
79113
export default router;

frontend/src/components/MultiNetworkGasBalances.tsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useState, useEffect } from 'react'
22
import api from '../services/api'
33
import { countries } from '../utils/countries'
4-
import { formatTokenUsdEstimate } from '../utils/tokenUsd'
54

65
interface NetworkInfo {
76
name?: string;
@@ -35,6 +34,14 @@ interface MultiNetworkStatus {
3534
totalReserve?: number;
3635
}
3736

37+
interface TokenPriceQuote {
38+
symbol: string;
39+
coinId: string;
40+
usd: number;
41+
lastUpdatedAt: string | null;
42+
source: 'coingecko';
43+
}
44+
3845
type ExplorerLinkConfig = {
3946
label: string;
4047
buildUrl: (address: string) => string;
@@ -107,10 +114,17 @@ function MultiNetworkGasBalances() {
107114
const [loadingNetworks, setLoadingNetworks] = useState<Record<string, boolean>>({})
108115
const [error, setError] = useState<string | null>(null)
109116
const [copiedAddressKey, setCopiedAddressKey] = useState<string | null>(null)
117+
const [tokenQuotes, setTokenQuotes] = useState<Record<string, TokenPriceQuote>>({})
110118

111119
const [scope, setScope] = useState<'GLOBAL' | 'COUNTRY'>('GLOBAL');
112120
const [selectedCountry, setSelectedCountry] = useState<string>('DE'); // Default to Germany or commonly used
113121

122+
const usdFormatter = new Intl.NumberFormat('en-US', {
123+
style: 'currency',
124+
currency: 'USD',
125+
maximumFractionDigits: 2
126+
})
127+
114128
const shortenAddress = (value: string, startLength = 6, endLength = 4) => {
115129
if (!value || value === 'N/A') {
116130
return value
@@ -151,6 +165,7 @@ function MultiNetworkGasBalances() {
151165
setError(null)
152166
setNetworkStatus(null)
153167
setLoadingNetworks({})
168+
setTokenQuotes({})
154169

155170
const params = new URLSearchParams()
156171
if (currentScope === 'COUNTRY') {
@@ -178,7 +193,7 @@ function MultiNetworkGasBalances() {
178193
networkName: net.networkName,
179194
adapterType: net.adapterType,
180195
walletAddress: net.walletAddress,
181-
tokenSymbol: '...',
196+
tokenSymbol: net.tokenSymbol || '...',
182197
totalReserve: 0,
183198
walletBalance: 0,
184199
availableForDistribution: 0,
@@ -199,6 +214,31 @@ function MultiNetworkGasBalances() {
199214
setLoadingNetworks(initialLoading)
200215
setLoading(false)
201216

217+
const tokenSymbols = Array.from(
218+
new Set(
219+
networks
220+
.map((net: any) => typeof net.tokenSymbol === 'string' ? net.tokenSymbol.trim().toUpperCase() : '')
221+
.filter((symbol: string) => symbol.length > 0)
222+
)
223+
)
224+
225+
if (tokenSymbols.length > 0) {
226+
try {
227+
const priceResponse = await api.get('/api/global/token-prices', {
228+
params: {
229+
symbols: tokenSymbols.join(',')
230+
}
231+
})
232+
233+
const quotes = priceResponse.data?.data?.quotes
234+
if (quotes && typeof quotes === 'object') {
235+
setTokenQuotes(quotes)
236+
}
237+
} catch (priceError) {
238+
console.error('Failed to fetch live token prices:', priceError)
239+
}
240+
}
241+
202242
// 2. Fetch each network's status individualy in parallel (backend will coalesce and cache)
203243
enabledNetworks.forEach(async (networkId: string) => {
204244
try {
@@ -250,6 +290,31 @@ function MultiNetworkGasBalances() {
250290
}
251291
}
252292

293+
const formatUsdAmount = (tokenSymbol: string, amount?: number) => {
294+
if (!Number.isFinite(amount)) {
295+
return null
296+
}
297+
298+
const quote = tokenQuotes[tokenSymbol.trim().toUpperCase()]
299+
if (!quote || !Number.isFinite(quote.usd)) {
300+
return null
301+
}
302+
303+
return usdFormatter.format((amount as number) * quote.usd)
304+
}
305+
306+
const latestQuoteTime = Object.values(tokenQuotes).reduce<string | null>((latest, quote) => {
307+
if (!quote.lastUpdatedAt) {
308+
return latest
309+
}
310+
311+
if (!latest || new Date(quote.lastUpdatedAt).getTime() > new Date(latest).getTime()) {
312+
return quote.lastUpdatedAt
313+
}
314+
315+
return latest
316+
}, null)
317+
253318
useEffect(() => {
254319
fetchMultiNetworkStatus(scope, selectedCountry)
255320
}, [scope, selectedCountry])
@@ -356,9 +421,11 @@ function MultiNetworkGasBalances() {
356421
<p style={{ margin: '0.5rem 0 0 0', color: '#0c4a6e', fontSize: '0.9rem' }}>
357422
{networkStatus.totalNetworks} networks enabled: {networkStatus.enabledNetworks.join(', ')}
358423
</p>
359-
<p style={{ margin: '0.5rem 0 0 0', color: '#075985', fontSize: '0.8rem' }}>
360-
USD values are reference estimates from a static price table, not live market quotes.
361-
</p>
424+
{latestQuoteTime && (
425+
<p style={{ margin: '0.5rem 0 0 0', color: '#075985', fontSize: '0.8rem' }}>
426+
USD quotes from CoinGecko. Last update: {new Date(latestQuoteTime).toLocaleString()}
427+
</p>
428+
)}
362429
</div>
363430

364431
{/* Network Details */}
@@ -399,10 +466,14 @@ function MultiNetworkGasBalances() {
399466
const balanceDisplay = balanceFormatted === 'N/A'
400467
? (isNetworkLoading ? 'Loading...' : 'N/A')
401468
: `${balanceFormatted} ${tokenSymbol}`
402-
const balanceUsdEstimate = formatTokenUsdEstimate(tokenSymbol, fallbackWalletBalance)
469+
const balanceUsdEstimate = formatUsdAmount(tokenSymbol, fallbackWalletBalance)
403470
const gasPriceDisplay = gasPriceFormatted === 'N/A'
404471
? (isNetworkLoading ? 'Loading...' : 'N/A')
405472
: `${gasPriceFormatted} ${tokenSymbol}`
473+
const gasPriceNumeric = Number.parseFloat(gasPriceFormatted)
474+
const gasPriceUsdEstimate = Number.isFinite(gasPriceNumeric)
475+
? formatUsdAmount(tokenSymbol, gasPriceNumeric)
476+
: null
406477

407478
return (
408479
<div key={networkName} style={{
@@ -450,6 +521,9 @@ function MultiNetworkGasBalances() {
450521
</p>
451522
<p style={{ margin: '0.25rem 0', color: '#888' }}>
452523
<strong>Gas Price:</strong> {gasPriceDisplay}
524+
{gasPriceUsdEstimate && (
525+
<span style={{ color: '#cbd5e1' }}> ({gasPriceUsdEstimate})</span>
526+
)}
453527
</p>
454528
<p style={{ margin: '0.25rem 0', color: '#888' }}>
455529
<strong>Address:</strong>{" "}

frontend/src/services/api.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ interface SalaryStats {
109109
medianRecommendedSalary: number;
110110
}
111111

112+
interface TokenPriceQuote {
113+
symbol: string;
114+
coinId: string;
115+
usd: number;
116+
lastUpdatedAt: string | null;
117+
source: 'coingecko';
118+
}
119+
112120
interface AuthData {
113121
ethereumAddress?: string;
114122
signature?: string;
@@ -276,6 +284,11 @@ export const worldGdpApi = {
276284
api.get('/api/global/gdp'),
277285
}
278286

287+
export const tokenPricesApi = {
288+
get: (symbols: string[]): Promise<AxiosResponse<{ success: boolean; data: { quotes: Record<string, TokenPriceQuote>; source: string; requestedSymbols: string[]; supportedSymbols: string[] } }>> =>
289+
api.get('/api/global/token-prices', { params: { symbols: symbols.join(',') } }),
290+
}
291+
279292
// Add response interceptor for error handling
280293
api.interceptors.response.use(
281294
(response) => response,
@@ -286,4 +299,4 @@ api.interceptors.response.use(
286299
)
287300

288301
export default api
289-
export type { User, Post, CreateUserData, CreatePostData, UpdateUserData, UpdatePostData, AuthData, DBLogEntry, LogsFilter, LogStats, LogTypes, LeaderboardEntry, SalaryStats }
302+
export type { User, Post, CreateUserData, CreatePostData, UpdateUserData, UpdatePostData, AuthData, DBLogEntry, LogsFilter, LogStats, LogTypes, LeaderboardEntry, SalaryStats, TokenPriceQuote }

git_hooks/pre-commit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ npm test
77

88
cd backend
99

10-
# TODO@P2: hack
11-
# npm update prisma
10+
# To avoid prisma errors.
11+
npm run db:generate
1212

1313
# Prevent submitting, if has not been tested on a local DB.
1414
npx prisma migrate status --config prisma/prisma.config.js

0 commit comments

Comments
 (0)