import { Satoshis } from '@xocash/primitives'; import { BaseRates, type RateOptions } from './base-rates.js'; import { Memoizer } from '../utils/index.js'; /** * Represents a single market product (pair) on Coinbase Exchange. */ export interface CoinbaseProduct { id: string; base_currency: string; quote_currency: string; base_min_size: string; base_max_size: string; quote_increment: string; } /** * Dependencies for RatesCoinbase. */ export type RatesCoinbaseDependencies = { /** * Base URL for Coinbase Exchange API (defaults to prod). */ apiBaseUrl: string; /** * Optional cache instance for rate lookups. */ cache: Memoizer; /** * Optional cache TTL for rate lookups. */ cacheTTL: number; /** * Optional fetch-like function (e.g. global fetch) for HTTP calls. */ fetch: (input: RequestInfo, init?: RequestInit) => Promise; }; /** * CoinbaseRates * * Implements BaseRates by calling Coinbase Exchange's public REST endpoints. */ export class RatesCoinbase extends BaseRates { private apiBaseUrl: string; private cache: Memoizer; private cacheTTL: number; private fetch: (input: RequestInfo, init?: RequestInit) => Promise; private constructor(deps: Partial) { super(); this.apiBaseUrl = deps.apiBaseUrl ?? 'https://api.exchange.coinbase.com'; this.cache = deps.cache ?? new Memoizer(); this.cacheTTL = deps.cacheTTL ?? 60 * 1000; this.fetch = deps.fetch ?? fetch.bind(globalThis); } /** * Factory to create a CoinbaseRates instance. */ public static from( apiBaseUrl: string = 'https://api.exchange.coinbase.com', cacheTTL: number = 60 * 1000, fetch?: (input: RequestInfo, init?: RequestInit) => Promise, ): RatesCoinbase { const cache = new Memoizer(); return new RatesCoinbase({ apiBaseUrl, cache, cacheTTL, fetch }); } /** No persistent connection to open. */ public async start(): Promise {} /** No persistent connection to close. */ public async stop(): Promise {} /** * List all available market products (pairs). */ public async listProducts(): Promise { const url = `${this.apiBaseUrl}/products`; const res = await this.fetch(url); if (!res.ok) { throw new Error(`Failed to fetch products (${res.status}): ${res.statusText}`); } return (await res.json()) as CoinbaseProduct[]; } /** * Get the raw exchange rate for a currency pair. */ public async getRate( currency: string, options?: Partial, ): Promise { const fetchPair = async (pair: string): Promise => { return await this.cache.get(pair, async () => { const price = await this.fetchTickerPrice(pair); this.cache.set(pair, price); return price; }, this.cacheTTL); }; const direct = `${currency}-BCH`; try { const denominatorRate = await fetchPair(direct); return 1 / denominatorRate; } catch { const inverse = `BCH-${currency}`; const invPrice = await fetchPair(inverse); return invPrice; } } /** * Convert an amount in a foreign currency to BCH. */ public async toBCH( amount: number, fromCurrency: string, options?: Partial, ): Promise { const rate = await this.getRate(fromCurrency, options); return Satoshis.fromBCH(amount / rate); } /** * Convert a BCH amount to a target currency. */ public async fromBCH( amount: Satoshis, targetCurrency: string, options?: Partial, ): Promise { const bchAmount = amount.toBCH(); const rate = await this.getRate(targetCurrency, options); return bchAmount * rate; } /** * Fetch the current ticker price for a given pair. */ private async fetchTickerPrice(pair: string): Promise { const url = `${this.apiBaseUrl}/products/${pair}/ticker`; const res = await this.fetch(url); if (!res.ok) { throw new Error(`Ticker API error (${res.status}): ${res.statusText}`); } const data = (await res.json()) as { price: string }; return parseFloat(data.price); } }