Initial commit
This commit is contained in:
66
lib/api/crypto.ts
Normal file
66
lib/api/crypto.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { CRYPTO_ASSETS } from "@/lib/assets";
|
||||
|
||||
export interface CryptoRateResult {
|
||||
usdPrices: Record<string, number>;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3";
|
||||
|
||||
export async function fetchCryptoData(): Promise<CryptoRateResult> {
|
||||
const ids = CRYPTO_ASSETS.map((asset) => asset.providerId).filter(
|
||||
(id): id is string => Boolean(id)
|
||||
);
|
||||
|
||||
const url = `${COINGECKO_BASE_URL}/simple/price?ids=${encodeURIComponent(
|
||||
ids.join(",")
|
||||
)}&vs_currencies=usd&include_last_updated_at=true`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
next: { revalidate: 60 },
|
||||
headers: {
|
||||
accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load crypto rates (${response.status})`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as Record<
|
||||
string,
|
||||
{
|
||||
usd?: number;
|
||||
last_updated_at?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
const usdPrices: Record<string, number> = {};
|
||||
let mostRecentUpdate = 0;
|
||||
|
||||
for (const asset of CRYPTO_ASSETS) {
|
||||
if (!asset.providerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = payload[asset.providerId];
|
||||
|
||||
if (!entry?.usd || entry.usd <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usdPrices[asset.code] = entry.usd;
|
||||
|
||||
if (entry.last_updated_at && entry.last_updated_at > mostRecentUpdate) {
|
||||
mostRecentUpdate = entry.last_updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
usdPrices,
|
||||
updatedAt:
|
||||
mostRecentUpdate > 0
|
||||
? new Date(mostRecentUpdate * 1000).toISOString()
|
||||
: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
43
lib/api/fiat.ts
Normal file
43
lib/api/fiat.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface FiatRateResult {
|
||||
currencyNames: Record<string, string>;
|
||||
usdRates: Record<string, number>;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const FRANKFURTER_BASE_URL = "https://api.frankfurter.app";
|
||||
|
||||
export async function fetchFiatData(): Promise<FiatRateResult> {
|
||||
const [currenciesRes, latestRes] = await Promise.all([
|
||||
fetch(`${FRANKFURTER_BASE_URL}/currencies`, {
|
||||
next: { revalidate: 60 }
|
||||
}),
|
||||
fetch(`${FRANKFURTER_BASE_URL}/latest?from=USD`, {
|
||||
next: { revalidate: 60 }
|
||||
})
|
||||
]);
|
||||
|
||||
if (!currenciesRes.ok) {
|
||||
throw new Error(`Unable to load fiat currency names (${currenciesRes.status})`);
|
||||
}
|
||||
|
||||
if (!latestRes.ok) {
|
||||
throw new Error(`Unable to load fiat rates (${latestRes.status})`);
|
||||
}
|
||||
|
||||
const currencyNames = (await currenciesRes.json()) as Record<string, string>;
|
||||
const latest = (await latestRes.json()) as {
|
||||
date: string;
|
||||
rates: Record<string, number>;
|
||||
};
|
||||
|
||||
const usdRates: Record<string, number> = {
|
||||
USD: 1,
|
||||
...latest.rates
|
||||
};
|
||||
|
||||
return {
|
||||
currencyNames,
|
||||
usdRates,
|
||||
updatedAt: new Date(`${latest.date}T00:00:00Z`).toISOString()
|
||||
};
|
||||
}
|
||||
58
lib/api/normalize.ts
Normal file
58
lib/api/normalize.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { buildAllAssets } from "@/lib/assets";
|
||||
import { fetchCryptoData } from "@/lib/api/crypto";
|
||||
import { fetchFiatData } from "@/lib/api/fiat";
|
||||
import { RateAsset, RatesResponse } from "@/lib/rates";
|
||||
|
||||
function normalizeFiatUsdPrice(usdRate: number): number {
|
||||
return 1 / usdRate;
|
||||
}
|
||||
|
||||
export async function fetchUnifiedRates(): Promise<RatesResponse> {
|
||||
const [fiat, crypto] = await Promise.all([fetchFiatData(), fetchCryptoData()]);
|
||||
const assetDefinitions = buildAllAssets(fiat.currencyNames);
|
||||
|
||||
const assets: RateAsset[] = [];
|
||||
|
||||
for (const asset of assetDefinitions) {
|
||||
if (asset.type === "fiat") {
|
||||
const usdRate = fiat.usdRates[asset.code];
|
||||
|
||||
if (!usdRate || usdRate <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assets.push({
|
||||
...asset,
|
||||
usdPrice: normalizeFiatUsdPrice(usdRate),
|
||||
updatedAt: fiat.updatedAt
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const usdPrice = crypto.usdPrices[asset.code];
|
||||
|
||||
if (!usdPrice || usdPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assets.push({
|
||||
...asset,
|
||||
usdPrice,
|
||||
updatedAt: crypto.updatedAt
|
||||
});
|
||||
}
|
||||
|
||||
const fiatUpdated = new Date(fiat.updatedAt).getTime();
|
||||
const cryptoUpdated = new Date(crypto.updatedAt).getTime();
|
||||
|
||||
return {
|
||||
assets,
|
||||
quoteCurrency: "USD",
|
||||
updatedAt: new Date(Math.max(fiatUpdated, cryptoUpdated)).toISOString(),
|
||||
sources: {
|
||||
fiat: "Frankfurter",
|
||||
crypto: "CoinGecko"
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user