import { BaseRates } from './base-rates.js'; import { z } from 'zod'; /** * CryptoCompareRates * * Implements BaseRates by calling CryptoCompare's public REST endpoints. * CryptoCompare is CORS-friendly and works well in browser environments. */ export class RatesCryptoCompare extends BaseRates { private readonly currencyCodes: Array; private readonly opts: RatesCryptoCompareOptions; // Mutable state. // NOTE: Whether this exists indicates whether this service is running. private refreshTimer?: ReturnType; // Rates currently available. // Format is: denominatorUnitCode -> Set private availablePairs: Map> = new Map(); // Supported fiat currencies from CryptoCompare private supportedFiats: Set = new Set(); private constructor( currencyCodes: Array, opts: Partial, ) { super(); this.currencyCodes = currencyCodes; this.opts = { // Default to CryptoCompare API URL. baseURL: 'https://min-api.cryptocompare.com/data', // Default to 60 second refresh. refreshIntervalMs: 60_000, // Default list of target currencies if none specified targetFiats: ['USD', 'EUR', 'GBP', 'JPY', 'CNY', 'AUD', 'CAD', 'CHF'], // Default to browser/node fetch function fetch: (...args) => globalThis.fetch(...args), // API key is optional but recommended apiKey: undefined, // Merge any user-specified options in. ...opts, }; // Initialize the available pairs map this.availablePairs = new Map(); // Initialize supported fiats with the default/specified target fiats this.supportedFiats = new Set( this.opts.targetFiats.map((fiat) => fiat.toUpperCase()), ); } /** * Factory to create a CryptoCompareRates instance. */ public static from( currencyCodes: Array = ['BCH'], opts: Partial = {}, ): RatesCryptoCompare { return new RatesCryptoCompare(currencyCodes, opts); } public async start(): Promise { // Initialize by getting supported fiat currencies await this.getSupportedCurrencies(); // Initial refresh to populate available pairs await this.refreshPrices(); // Set a timer to refresh prices at the given interval. this.refreshTimer = setTimeout( this.refreshPrices.bind(this), this.opts.refreshIntervalMs, ); } public async stop(): Promise { clearTimeout(this.refreshTimer); this.refreshTimer = undefined; } public async listPairs(): Promise> { // Refresh prices first to make sure that we have an accurate picture of supported pairs. if (!this.refreshTimer) { await this.refreshPrices(); } const formattedPairs = new Set(); // Convert our internal representation to the standard "NUMERATOR/DENOMINATOR" format for (const [denominator, numerators] of this.availablePairs.entries()) { for (const numerator of numerators) { formattedPairs.add(`${numerator}/${denominator}`); } } return formattedPairs; } private async getSupportedCurrencies(): Promise { try { // CryptoCompare has an endpoint to list all supported currencies const endpoint = `${this.opts.baseURL}/all/coinlist`; // Prepare headers if API key is provided const headers: HeadersInit = {}; if (this.opts.apiKey) { headers['authorization'] = `Apikey ${this.opts.apiKey}`; } const response = await this.opts.fetch(endpoint, { headers }); if (!response.ok) { throw new Error( `Failed to fetch supported currencies: ${response.statusText}`, ); } const data = await response.json(); // CryptoCompare returns a complex object, verify that Data property exists if (data.Response === 'Success' && data.Data) { // We've successfully retrieved the currency list // This is primarily to confirm our crypto currencies exist // but we don't need to do anything with this data right now } } catch (error) { // Log error but continue - we'll use default target fiats console.error('Error fetching supported currencies:', error); } } private async refreshPrices() { // For each crypto we're interested in, get prices for all target fiats in a single call for (const cryptoCode of this.currencyCodes) { try { // The fsyms parameter is the crypto code(s) // The tsyms parameter is a comma-separated list of all target fiat currencies const endpoint = `${this.opts.baseURL}/price?fsym=${cryptoCode.toUpperCase()}&tsyms=${Array.from( this.supportedFiats, ).join(',')}`; // Prepare headers if API key is provided const headers: HeadersInit = {}; if (this.opts.apiKey) { headers['authorization'] = `Apikey ${this.opts.apiKey}`; } // Fetch from CryptoCompare API const pricesRes = await this.opts.fetch(endpoint, { headers }); // If request failed, throw an error if (!pricesRes.ok) { throw new Error( `Failed to fetch rates for ${cryptoCode}: ${pricesRes.statusText}`, ); } // Extract the data - CryptoCompare returns a simple object with fiat codes as keys const data = CryptoComparePriceSchema.parse(await pricesRes.json()); // Initialize the set for this crypto if it doesn't exist if (!this.availablePairs.has(cryptoCode)) { this.availablePairs.set(cryptoCode, new Set()); } // Current fiat currencies for this crypto const currentFiats = new Set(); // Process each rate entry for (const [fiatCode, rateValue] of Object.entries(data)) { const upperFiatCode = fiatCode.toUpperCase(); // Add this pair to our available pairs currentFiats.add(upperFiatCode); // Emit an event to indicate this rate has been updated this.emit('rateUpdated', { denominatorUnitCode: cryptoCode, numeratorUnitCode: upperFiatCode, price: rateValue, }); } // Update the available pairs for this crypto this.availablePairs.set(cryptoCode, currentFiats); } catch (error) { // Log the error for debugging console.error(`Error refreshing ${cryptoCode} prices:`, error); // This pair is no longer available, so delete it from available pairs this.availablePairs.delete(cryptoCode); } } // Schedule the next refresh if (this.refreshTimer) { this.refreshTimer = setTimeout( this.refreshPrices.bind(this), this.opts.refreshIntervalMs, ); } } } /** * Options for RatesCryptoCompare. */ export type RatesCryptoCompareOptions = { /** * Base URL for CryptoCompare API. */ baseURL: string; /** * Refresh interval in milliseconds. */ refreshIntervalMs: number; /** * List of target fiat currencies to fetch prices for. */ targetFiats: Array; /** * Optional API key for CryptoCompare (increases rate limits) */ apiKey?: string; /** * Optional fetch-like function (e.g. global fetch) for HTTP calls. */ fetch: (url: string, options?: RequestInit) => Promise; }; // Schema for the price response from CryptoCompare // This is a simple object with fiat currency codes as keys // and price values as numbers const CryptoComparePriceSchema = z.record(z.string(), z.number());