feat(Rates): Add CryptoCompare adapter
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
export * from './base-rates.js';
|
export * from './base-rates.js';
|
||||||
export * from './rates-bitpay.js';
|
export * from './rates-bitpay.js';
|
||||||
export * from './rates-coinbase.js';
|
export * from './rates-coinbase.js';
|
||||||
|
export * from './rates-cryptocompare.js';
|
||||||
export * from './rates-composite.js';
|
export * from './rates-composite.js';
|
||||||
export * from './traits.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