@mixxtor/currencyx-js
Version:
Modern TypeScript currency converter with type inference and multiple providers (Google Finance, Fixer.io). Framework agnostic with clean architecture.
199 lines (171 loc) • 5.06 kB
text/typescript
/**
* Fixer.io Exchange
*/
import type {
CurrencyCode,
ConversionResult,
ExchangeRatesResult,
FixerConfig,
ExchangeRatesParams,
ConvertParams,
} from '../types/index.js'
import { BaseCurrencyExchange } from './base_exchange.js'
interface FixerResponse {
success: boolean
timestamp?: number
base?: string
date?: string
rates?: Record<string, number>
error?: {
code: number
info: string
type: string
}
}
interface FixerConvertResponse {
success: boolean
query?: {
from: string
to: string
amount: number
}
info?: {
timestamp: number
rate: number
}
date?: string
result?: number
error?: {
code: number
info: string
type: string
}
}
export class FixerExchange extends BaseCurrencyExchange {
readonly name = 'fixer'
private baseUrl = 'http://data.fixer.io/api'
private accessKey: string
private timeout: number
constructor(config: FixerConfig) {
super()
this.accessKey = config.accessKey
this.base = config.base || 'EUR' // Fixer.io default base is EUR
this.timeout = config.timeout || 5000
}
/**
* Set API key
*/
setKey(key: string): this {
this.accessKey = key
return this
}
/**
* Get latest exchange rates
*/
async latestRates(params?: ExchangeRatesParams): Promise<ExchangeRatesResult> {
const codes = params?.codes || this.currencies
try {
const url = new URL(`${this.baseUrl}/latest`)
url.searchParams.set('access_key', this.accessKey)
url.searchParams.set('base', this.base)
if (codes && codes.length > 0) {
url.searchParams.set('symbols', codes.join(','))
}
const response = await fetch(url.toString(), {
signal: AbortSignal.timeout(this.timeout),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: FixerResponse = await response.json()
if (!data.success) {
return this.createExchangeRatesResult(
this.base,
{},
{
code: data.error?.code,
info: data.error?.info || 'Unknown error from Fixer.io',
type: data.error?.type || 'API_ERROR',
}
)
}
return this.createExchangeRatesResult(this.base, data.rates || {})
} catch (error) {
return this.createExchangeRatesResult(
this.base,
{},
{
info: error instanceof Error ? error.message : 'Failed to fetch exchange rates',
type: 'FETCH_ERROR',
}
)
}
}
/**
* Convert currency amount
*/
async convert(params: ConvertParams): Promise<ConversionResult> {
const { amount, from, to } = params
try {
if (from === to) {
return this.createConversionResult(amount, from, to, amount, 1.0)
}
const url = new URL(`${this.baseUrl}/convert`)
url.searchParams.set('access_key', this.accessKey)
url.searchParams.set('from', from)
url.searchParams.set('to', to)
url.searchParams.set('amount', amount.toString())
const response = await fetch(url.toString(), {
signal: AbortSignal.timeout(this.timeout),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: FixerConvertResponse = await response.json()
if (!data.success) {
return this.createConversionResult(amount, from, to, undefined, undefined, {
code: data.error?.code,
info: data.error?.info || 'Unknown error from Fixer.io',
type: data.error?.type || 'API_ERROR',
})
}
return this.createConversionResult(amount, from, to, data.result, data.info?.rate)
} catch (error) {
return this.createConversionResult(amount, from, to, undefined, undefined, {
info: error instanceof Error ? error.message : 'Conversion failed',
type: 'CONVERSION_ERROR',
})
}
}
/**
* Get conversion rate between two currencies
*/
async getConvertRate(from: CurrencyCode, to: CurrencyCode): Promise<number | undefined> {
try {
if (from === to) {
return 1.0
}
// If one of the currencies is the base currency, we can use latest rates
if (from === this.base) {
const rates = await this.latestRates({ codes: [to] })
return rates.rates[to]
}
if (to === this.base) {
const rates = await this.latestRates({ codes: [from] })
const rate = rates.rates[from]
return rate ? 1 / rate : undefined
}
// For cross-currency conversion, get both rates against base
const rates = await this.latestRates({ codes: [from, to] })
const fromRate = rates.rates[from]
const toRate = rates.rates[to]
if (fromRate && toRate) {
return toRate / fromRate
}
return undefined
} catch (error) {
console.error(`Fixer: Failed to get conversion rate for ${from}-${to}:`, error)
return undefined
}
}
}