diff --git a/src/monitoringV2/contract.service.ts b/src/monitoringV2/contract.service.ts index 3801f37..581caa5 100644 --- a/src/monitoringV2/contract.service.ts +++ b/src/monitoringV2/contract.service.ts @@ -100,7 +100,7 @@ export class ContractService { ); } - await this.contractRepo.createMany(coreContracts); + await this.contractRepo.upsertCore(coreContracts); this.logger.log(`Registry initialized with ${coreContracts.length} core contracts`); this.logger.log(`Registered addresses: ${coreContracts.map((c) => c.address).join(', ')}`); } diff --git a/src/monitoringV2/price.service.ts b/src/monitoringV2/price.service.ts index 20bee81..8ee6e2d 100644 --- a/src/monitoringV2/price.service.ts +++ b/src/monitoringV2/price.service.ts @@ -106,36 +106,26 @@ export class PriceService { const baseUrl = this.appConfigService.geckoTerminalBaseUrl; - try { - const response = await axios.get( - `${baseUrl}/api/v2/simple/networks/eth/token_price/${remaining.map((a) => a.toLowerCase()).join(',')}`, - { - headers: { accept: 'application/json' }, - timeout: 10000, // 10 second timeout - } - ); - - const apiPrices = response.data.data.attributes.token_prices; - const normalizedPrices: { [key: string]: string } = {}; - for (const inputAddress of remaining) { - const price = apiPrices[inputAddress.toLowerCase()]; - if (price) { - normalizedPrices[inputAddress] = price; - this.setCache(inputAddress, price); - } + const response = await axios.get( + `${baseUrl}/api/v2/simple/networks/eth/token_price/${remaining.map((a) => a.toLowerCase()).join(',')}`, + { + headers: { accept: 'application/json' }, + timeout: 10000, // 10 second timeout } + ); - this.logger.log(`Fetched prices for ${Object.keys(normalizedPrices).length} tokens from GeckoTerminal`); - return { ...cached, ...normalizedPrices }; - } catch (error) { - this.logger.error('Failed to fetch token prices from GeckoTerminal:', error); - if (cached) { - this.logger.warn('Returning expired cached prices due to API error'); - return cached; + const apiPrices = response.data.data.attributes.token_prices; + const normalizedPrices: { [key: string]: string } = {}; + for (const inputAddress of remaining) { + const price = apiPrices[inputAddress.toLowerCase()]; + if (price) { + normalizedPrices[inputAddress] = price; + this.setCache(inputAddress, price); } - - return {}; } + + this.logger.log(`Fetched prices for ${Object.keys(normalizedPrices).length} tokens from GeckoTerminal`); + return { ...cached, ...normalizedPrices }; } private async getSpecialTokenPrices(requestedAddresses: string[]): Promise<{ [key: string]: string }> { @@ -191,7 +181,7 @@ export class PriceService { // Deduplicate concurrent requests if (this.pendingFxRates) return this.pendingFxRates; - this.pendingFxRates = this.fetchFxRates(eurCached, chfCached); + this.pendingFxRates = this.fetchFxRates(); try { return await this.pendingFxRates; } finally { @@ -226,49 +216,28 @@ export class PriceService { return { baseUrl, headers }; } - private async fetchFxRates( - eurCached: PriceCacheEntry | undefined, - chfCached: PriceCacheEntry | undefined - ): Promise<{ eur: number; chf: number }> { - try { - const { baseUrl, headers } = this.resolveCoingeckoEndpoint(); - const response = await axios.get(`${baseUrl}/api/v3/simple/price?ids=usd&vs_currencies=eur,chf`, { - headers, - timeout: 10000, - }); - - const eur = Number(response.data.usd.eur); - const chf = Number(response.data.usd.chf); - - if (Number.isNaN(eur) || Number.isNaN(chf)) { - this.logger.error('CoinGecko returned non-numeric FX rates', response.data); - return { - eur: !Number.isNaN(eur) ? eur : eurCached ? Number(eurCached.value) : 1, - chf: !Number.isNaN(chf) ? chf : chfCached ? Number(chfCached.value) : 1, - }; - } - - const now = Date.now(); - this.priceCache.set('usd-eur-rate', { value: String(eur), timestamp: now }); - this.priceCache.set('usd-chf-rate', { value: String(chf), timestamp: now }); - this.fxLastSuccessMs = now; - this.fxStalenessAlertedAt = null; + private async fetchFxRates(): Promise<{ eur: number; chf: number }> { + const { baseUrl, headers } = this.resolveCoingeckoEndpoint(); + const response = await axios.get(`${baseUrl}/api/v3/simple/price?ids=usd&vs_currencies=eur,chf`, { + headers, + timeout: 10000, + }); - this.logger.debug(`FX rates: USD/EUR=${eur}, USD/CHF=${chf}`); - return { eur, chf }; - } catch (error) { - this.logger.error('Failed to fetch FX rates:', error.message || error); + const eur = Number(response.data?.usd?.eur); + const chf = Number(response.data?.usd?.chf); - const eur = eurCached ? Number(eurCached.value) : 1; - const chf = chfCached ? Number(chfCached.value) : 1; + if (!Number.isFinite(eur) || !Number.isFinite(chf) || eur <= 0 || chf <= 0) { + throw new Error(`CoinGecko returned invalid FX rates: usd.eur=${response.data?.usd?.eur}, usd.chf=${response.data?.usd?.chf}`); + } - // Refresh cache timestamps so we don't retry on every call while rate-limited - const now = Date.now(); - this.priceCache.set('usd-eur-rate', { value: String(eur), timestamp: now }); - this.priceCache.set('usd-chf-rate', { value: String(chf), timestamp: now }); + const now = Date.now(); + this.priceCache.set('usd-eur-rate', { value: String(eur), timestamp: now }); + this.priceCache.set('usd-chf-rate', { value: String(chf), timestamp: now }); + this.fxLastSuccessMs = now; + this.fxStalenessAlertedAt = null; - return { eur, chf }; - } + this.logger.debug(`FX rates: USD/EUR=${eur}, USD/CHF=${chf}`); + return { eur, chf }; } private isSpecialToken(address: string): boolean { diff --git a/src/monitoringV2/prisma/repositories/contract.repository.ts b/src/monitoringV2/prisma/repositories/contract.repository.ts index 6ca5ea3..3cf233a 100644 --- a/src/monitoringV2/prisma/repositories/contract.repository.ts +++ b/src/monitoringV2/prisma/repositories/contract.repository.ts @@ -30,6 +30,28 @@ export class ContractRepository { } } + // Core contracts are the canonical, hard-coded protocol addresses from + // @deuro/eurocoin. Their `type` is authoritative and must override any + // earlier classification (e.g. a Savings deployed via MinterApplied was + // first persisted as generic MINTER, then later promoted to SAVINGS_V3 + // once the package shipped its address). Use upsert so re-registration + // corrects the type instead of being silently skipped. + async upsertCore(contracts: Contract[]): Promise { + if (contracts.length === 0) return; + + for (const contract of contracts) { + const address = contract.address.toLowerCase(); + const metadata = contract.metadata || {}; + await this.prisma.contract.upsert({ + where: { address }, + create: { address, type: contract.type, timestamp: contract.timestamp, metadata }, + update: { type: contract.type, metadata }, + }); + } + + this.logger.log(`Upserted ${contracts.length} core contracts`); + } + async findAll(): Promise { try { const contracts = await this.prisma.contract.findMany({