UNPKG

soroswap-utils

Version:

Utilities for interacting with Soroswap, the decentralized exchange (DEX) on Soroban, which is the smart contracts platform of the Stellar network.

175 lines (143 loc) 5.63 kB
import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { getConfig } from "./config"; import type { Asset, AssetData, CacheEntry, MainnetResponse, SimpleAsset, TestnetData, TestnetResponse, } from "./types"; const cacheDirectory = join(process.cwd(), "node_modules", "soroswap-utils", ".cache"); // eslint-disable-next-line unicorn/prefer-top-level-await void (async () => { try { await access(cacheDirectory); } catch { // eslint-disable-next-line @typescript-eslint/naming-convention await mkdir(cacheDirectory, { recursive: true }); } })(); const mainnetCacheFile = join(cacheDirectory, "assets.json"); const testnetCacheFile = join(cacheDirectory, "testnet-assets.json"); const daysToCache = 30; const hoursPerDay = 24; const minutesPerHour = 60; const secondsPerMinute = 60; const millisecondsPerSecond = 1000; const cacheTtl = daysToCache * hoursPerDay * minutesPerHour * secondsPerMinute * millisecondsPerSecond; const extractTestnetData = (data: TestnetResponse): TestnetData => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion data.find((item: Readonly<TestnetData>): boolean => item.network === "testnet")!; // // Besides fetching fresh data, we also overwrite the cache file. // const fetchAssets = async (): Promise<AssetData> => { // The env var name is the same regardless of the network; the command line // sets the correct environment. const config = getConfig(); const response = await fetch(config.assets.url); const dataFromResponse = (await response.json()) as MainnetResponse | TestnetResponse; const isTestnet = config.rpc.url.includes("testnet"); const data = isTestnet ? extractTestnetData(dataFromResponse as TestnetResponse) : (dataFromResponse as MainnetResponse); // We will consider as certified only the mainnet assets. const certifiedData = { ...data, assets: data.assets.map((asset) => ({ ...asset, isSoroswapCertified: !isTestnet })), }; await writeFile( isTestnet ? testnetCacheFile : mainnetCacheFile, // Rule disabled because neither I nor Claude AI are smart enough to // figure out how to make this work. // eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment JSON.stringify({ data: certifiedData, timestamp: Date.now() }), ); return data; }; const readCache = async (): Promise<CacheEntry> => { const isTestnet = getConfig().rpc.url.includes("testnet"); const content = await readFile(isTestnet ? testnetCacheFile : mainnetCacheFile, "utf8"); return JSON.parse(content) as CacheEntry; }; const getCachedOrFetch = async (): Promise<AssetData> => { try { const cache = await readCache(); const isDataFresh = Date.now() - cache.timestamp < cacheTtl; if (!isDataFresh) { return await fetchAssets(); } const isTestnet = getConfig().rpc.url.includes("testnet"); if (!isTestnet) { return cache.data as MainnetResponse; } return cache.data as TestnetData; } catch { return await fetchAssets(); } }; const simplifyAssets = (data: AssetData): { assets: SimpleAsset[] } => ({ assets: data.assets.map(({ code, contract, isSoroswapCertified, issuer }) => ({ code, contract, isSoroswapCertified, issuer, })), }); /** * Fetches the list of certified assets from the Soroswap token list. * Assets are cached for 30 days. * @param shouldReturnSimpleAssets If true, returns a simpler object with only * the code and issuer. * @returns A promise that resolves to the list of certified assets. * @throws If assets cannot be fetched or cached. */ const listCertifiedAssets = async ( shouldReturnSimpleAssets = false, ): Promise<AssetData | { assets: SimpleAsset[] }> => { try { const data: AssetData = await getCachedOrFetch(); return shouldReturnSimpleAssets ? simplifyAssets(data) : data; } catch (error) { console.error("Failed to get assets:", error); throw error; } }; /** * Checks if a given asset is certified by Soroswap. * @param code The asset code. * @param contract The address of the asset contract. * @returns A promise that resolves to true if the asset is certified. */ const isCertifiedAsset = async (code: string, contract: string): Promise<boolean> => { if (code === "XLM" && contract === "Native") { return true; } const { assets } = await listCertifiedAssets(); return assets.some( (asset: { readonly code: string; readonly contract: string }) => asset.code === code && asset.contract === contract, ); }; /** * Retrieves data about an asset. * * @param contract The address of the asset's contract. * @returns A promise that resolves to the data about the asset. * @throws If asset not found (in mainnet only) */ const getAssetData = async (contract: string): Promise<Asset> => { const soroswapAssets = await getCachedOrFetch(); const assetData = soroswapAssets.assets.find((asset) => asset.contract === contract); // We have full data for a list of certified assets, but it is possible to // have other tokens in pools, and those we don't have data for. if (assetData === undefined) { return { contract, isSoroswapCertified: false }; } return assetData; }; export { getAssetData, getCachedOrFetch, isCertifiedAsset, listCertifiedAssets };