feat(Rates): Add CryptoCompare adapter
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export * from './base-rates.js';
|
||||
export * from './rates-bitpay.js';
|
||||
export * from './rates-coinbase.js';
|
||||
export * from './rates-cryptocompare.js';
|
||||
export * from './rates-composite.js';
|
||||
export * from './traits.js';
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
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<string>;
|
||||
private readonly opts: RatesCryptoCompareOptions;
|
||||
|
||||
// Mutable state.
|
||||
// NOTE: Whether this exists indicates whether this service is running.
|
||||
private refreshTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Rates currently available.
|
||||
// Format is: denominatorUnitCode -> Set<numeratorUnitCodes>
|
||||
private availablePairs: Map<string, Set<string>> = new Map();
|
||||
|
||||
// Supported fiat currencies from CryptoCompare
|
||||
private supportedFiats: Set<string> = new Set();
|
||||
|
||||
private constructor(
|
||||
currencyCodes: Array<string>,
|
||||
opts: Partial<RatesCryptoCompareOptions>,
|
||||
) {
|
||||
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<string> = ['BCH'],
|
||||
opts: Partial<RatesCryptoCompareOptions> = {},
|
||||
): RatesCryptoCompare {
|
||||
return new RatesCryptoCompare(currencyCodes, opts);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
// 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<void> {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = undefined;
|
||||
}
|
||||
|
||||
public async listPairs(): Promise<Set<string>> {
|
||||
// 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<string>();
|
||||
|
||||
// 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<void> {
|
||||
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<string>());
|
||||
}
|
||||
|
||||
// Current fiat currencies for this crypto
|
||||
const currentFiats = new Set<string>();
|
||||
|
||||
// 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<string>;
|
||||
|
||||
/**
|
||||
* 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<Response>;
|
||||
};
|
||||
|
||||
// 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());
|
||||
Reference in New Issue
Block a user