Added Rates Service: Base, Coinbase, Composite

This commit is contained in:
2025-04-28 12:36:30 +00:00
committed by Jim Hamill
parent c168d6124e
commit a0395514ba
8 changed files with 590 additions and 6 deletions
+156
View File
@@ -0,0 +1,156 @@
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<Response>;
};
/**
* 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<Response>;
private constructor(deps: Partial<RatesCoinbaseDependencies>) {
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<Response>,
): RatesCoinbase {
const cache = new Memoizer();
return new RatesCoinbase({ apiBaseUrl, cache, cacheTTL, fetch });
}
/** No persistent connection to open. */
public async start(): Promise<void> {}
/** No persistent connection to close. */
public async stop(): Promise<void> {}
/**
* List all available market products (pairs).
*/
public async listProducts(): Promise<CoinbaseProduct[]> {
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<RateOptions>,
): Promise<number> {
const fetchPair = async (pair: string): Promise<number> => {
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<RateOptions>,
): Promise<Satoshis> {
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<RateOptions>,
): Promise<number> {
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<number> {
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);
}
}