diff --git a/src/fiat-api/FiatApi.ts b/src/fiat-api/FiatApi.ts index bef864a..d0c8e6b 100644 --- a/src/fiat-api/FiatApi.ts +++ b/src/fiat-api/FiatApi.ts @@ -77,6 +77,10 @@ export enum FiatApiSupportedFiatCurrency { ZAR = 'zar', // South African Rand } +export enum FiatApiBridgedFiatCurrency { + CRC = 'crc', // Costa Rican Colón +} + const API_URL = 'https://api.coingecko.com/api/v3'; const COINGECKO_COIN_IDS = { [FiatApiSupportedCryptoCurrency.NIM]: 'nimiq-2', @@ -117,16 +121,70 @@ export async function getExchangeRates( */ export async function getHistoricExchangeRatesByRange( cryptoCurrency: FiatApiSupportedCryptoCurrency, - vsCurrency: FiatApiSupportedFiatCurrency | FiatApiSupportedCryptoCurrency, + vsCurrency: FiatApiSupportedFiatCurrency | FiatApiBridgedFiatCurrency | FiatApiSupportedCryptoCurrency, from: number, // in milliseconds to: number, // in milliseconds ): Promise> { + let bridgedCurrency: FiatApiBridgedFiatCurrency | undefined; + let bridgedExchangeRatePromise: Promise | null> = Promise.resolve(null); + if (Object.values(FiatApiBridgedFiatCurrency).includes(vsCurrency as FiatApiBridgedFiatCurrency)) { + bridgedCurrency = vsCurrency as FiatApiBridgedFiatCurrency; + + switch (bridgedCurrency) { + case FiatApiBridgedFiatCurrency.CRC: { + // Use USD as the intermediate currency + vsCurrency = FiatApiSupportedFiatCurrency.USD; + + // Adapt dates to Costa Rica timezone (UTC-6, all year round) + const fromDate = _timestampToUtcOffset(from, -6); + const toDate = _timestampToUtcOffset(to, -6); + // Get the day portion as ISO string + const fromDay = fromDate.toISOString().split('T')[0]; + const toDay = toDate.toISOString().split('T')[0]; + + bridgedExchangeRatePromise = _fetch( + `https://usd-crc-historic-rate.deno.dev/api/rates/${fromDay}/${toDay}`, + ); + break; + } + default: + throw new Error(`Unsupported bridged currency: ${bridgedCurrency}`); + } + } + const coinId = COINGECKO_COIN_IDS[cryptoCurrency.toLowerCase() as FiatApiSupportedCryptoCurrency]; // Note that from and to are expected in seconds but returned timestamps are in ms. from = Math.floor(from / 1000); to = Math.ceil(to / 1000); - const { prices: result } = await _fetch(`${API_URL}/coins/${coinId}/market_chart/range` - + `?vs_currency=${vsCurrency}&from=${from}&to=${to}`); + const [ + { prices: result }, + bridgedExchangeRates, + ] = await Promise.all([ + _fetch(`${API_URL}/coins/${coinId}/market_chart/range?vs_currency=${vsCurrency}&from=${from}&to=${to}`) as Promise<{ + prices: [number, number][], + }>, + bridgedExchangeRatePromise, + ]); + + if (bridgedCurrency && bridgedExchangeRates) { + for (let i = 0; i < result.length; ++i) { + const [timestamp, price] = result[i]; + switch (bridgedCurrency) { + case FiatApiBridgedFiatCurrency.CRC: { + // Adapt date to Costa Rica timezone (UTC-6, all year round) + const date = _timestampToUtcOffset(timestamp, -6); + // Get the day portion as ISO string + const day = date.toISOString().split('T')[0]; + // Convert from USD to CRC + result[i] = [timestamp, price * bridgedExchangeRates[day]]; + break; + } + default: + throw new Error(`Unsupported bridged currency: ${bridgedCurrency}`); + } + } + } + return result; } @@ -135,7 +193,7 @@ export async function getHistoricExchangeRatesByRange( */ export async function getHistoricExchangeRates( cryptoCurrency: FiatApiSupportedCryptoCurrency, - vsCurrency: FiatApiSupportedFiatCurrency | FiatApiSupportedCryptoCurrency, + vsCurrency: FiatApiSupportedFiatCurrency | FiatApiBridgedFiatCurrency | FiatApiSupportedCryptoCurrency, timestamps: number[], disableMinutlyData = false, ): Promise> { @@ -286,3 +344,9 @@ async function _fetch(input: RequestInfo, init?: RequestInit): Promise { } while (!result); return result; } + +function _timestampToUtcOffset(timestamp: number, utcOffset: number): Date { + const date = new Date(timestamp); + date.setHours(date.getHours() + utcOffset); + return date; +} diff --git a/tests/FiatApi.spec.ts b/tests/FiatApi.spec.ts new file mode 100644 index 0000000..27d2bc3 --- /dev/null +++ b/tests/FiatApi.spec.ts @@ -0,0 +1,64 @@ +/** + * @jest-environment node + */ + +/* global describe, it, expect */ + +import { + FiatApiBridgedFiatCurrency, + FiatApiSupportedCryptoCurrency, + FiatApiSupportedFiatCurrency, + getHistoricExchangeRates, +} from '../src/fiat-api/FiatApi'; + +describe('FiatApi', () => { + it('can fetch historic USD rates for BTC', async () => { + const timestamps = [ + new Date('2023-01-01T00:00:00.000Z').getTime(), + new Date('2023-01-01T01:00:00.000Z').getTime(), + new Date('2023-01-01T02:00:00.000Z').getTime(), + new Date('2023-01-01T03:00:00.000Z').getTime(), + new Date('2023-01-01T04:00:00.000Z').getTime(), + new Date('2023-10-13T05:00:00.000Z').getTime(), + new Date('2023-10-13T06:00:00.000Z').getTime(), + new Date('2023-10-13T07:00:00.000Z').getTime(), + new Date('2023-10-13T08:00:00.000Z').getTime(), + new Date('2023-10-13T09:00:00.000Z').getTime(), + ]; + const rates = await getHistoricExchangeRates( + FiatApiSupportedCryptoCurrency.BTC, + FiatApiSupportedFiatCurrency.USD, + timestamps, + ); + expect(rates.size).toBe(10); + expect(rates.get(timestamps[0])).toBe(16541.90475052885); + expect(rates.get(timestamps[1])).toBe(16543.017237311888); + expect(rates.get(timestamps[5])).toBe(26793.954797943756); + expect(rates.get(timestamps[6])).toBe(26810.776705117445); + }); + + it('can fetch historic CRC (bridged) rates for BTC', async () => { + const timestamps = [ + new Date('2023-01-01T00:00:00.000Z').getTime(), + new Date('2023-01-01T01:00:00.000Z').getTime(), + new Date('2023-01-01T02:00:00.000Z').getTime(), + new Date('2023-01-01T03:00:00.000Z').getTime(), + new Date('2023-01-01T04:00:00.000Z').getTime(), + new Date('2023-10-13T05:00:00.000Z').getTime(), + new Date('2023-10-13T06:00:00.000Z').getTime(), + new Date('2023-10-13T07:00:00.000Z').getTime(), + new Date('2023-10-13T08:00:00.000Z').getTime(), + new Date('2023-10-13T09:00:00.000Z').getTime(), + ]; + const rates = await getHistoricExchangeRates( + FiatApiSupportedCryptoCurrency.BTC, + FiatApiBridgedFiatCurrency.CRC, + timestamps, + ); + expect(rates.size).toBe(10); + expect(rates.get(timestamps[0])).toBe(9893382.393196296); + expect(rates.get(timestamps[1])).toBe(9894047.749291496); + expect(rates.get(timestamps[5])).toBe(14290555.791483302); + expect(rates.get(timestamps[6])).toBe(14244742.864549162); + }); +});