Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/monitoringV2/contract.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(', ')}`);
}
Expand Down
101 changes: 35 additions & 66 deletions src/monitoringV2/price.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,36 +106,26 @@ export class PriceService {

const baseUrl = this.appConfigService.geckoTerminalBaseUrl;

try {
const response = await axios.get<TokenPrice>(
`${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<TokenPrice>(
`${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 }> {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions src/monitoringV2/prisma/repositories/contract.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<Contract[]> {
try {
const contracts = await this.prisma.contract.findMany({
Expand Down
Loading