UNPKG

@paxoslabs/earn-sdk

Version:
703 lines (697 loc) 21.4 kB
import { defineChain } from 'viem'; import { swellchain, sei, plumeMainnet, form, boba, mainnet } from 'viem/chains'; // src/types/earn-sdk-api.ts var APIError = class _APIError extends Error { constructor(message, options) { super(message); this.name = "APIError"; this.statusCode = options?.statusCode; this.endpoint = options?.endpoint; this.cause = options?.cause; if (Error.captureStackTrace) { Error.captureStackTrace(this, _APIError); } } }; function isValidYieldType(value) { return typeof value === "string" && (value === "PRIME" || value === "TBILL" || value === "LENDING"); } function isValidAddress(value) { return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value); } function isValidChainId(value) { return typeof value === "number" && value > 0 && Number.isInteger(value); } // src/lib/vault-cache.ts var DEFAULT_TTL = 6e5; var VaultCache = class { /** * Creates a new VaultCache instance * * @param ttl - Time-to-live in milliseconds (default: 600000 = 10 minutes) */ constructor(ttl = DEFAULT_TTL) { this.vaults = /* @__PURE__ */ new Map(); this.assets = /* @__PURE__ */ new Map(); this.lastFetch = 0; this.ttl = ttl; this.refreshing = false; } /** * Gets vaults by deposit token address * * Returns undefined if no vaults found for the given token address. * Does NOT automatically refresh if cache is expired; use refresh() for that. * * @param tokenAddress - Deposit token address (baseTokenAddressId) * @returns Array of EarnVault objects, or undefined if not found */ getVault(tokenAddress) { return this.vaults.get(tokenAddress); } /** * Gets asset by token address * * Returns undefined if asset not found. * Does NOT automatically refresh if cache is expired; use refresh() for that. * * @param tokenAddress - Token address * @returns SupportedAsset object, or undefined if not found */ getAsset(tokenAddress) { return this.assets.get(tokenAddress); } /** * Gets all cached vaults * * Returns an array of all vaults across all deposit tokens. * Does NOT automatically refresh if cache is expired; use refresh() for that. * * @returns Array of all EarnVault objects */ getAllVaults() { const allVaults = []; for (const vaultArray of this.vaults.values()) { allVaults.push(...vaultArray); } return allVaults; } /** * Gets all cached assets * * Returns an array of all assets. * Does NOT automatically refresh if cache is expired; use refresh() for that. * * @returns Array of all SupportedAsset objects */ getAllAssets() { return Array.from(this.assets.values()); } /** * Checks if cache is expired * * Cache is considered expired if current time exceeds lastFetch + ttl. * * @returns true if cache is expired, false otherwise */ isExpired() { return Date.now() > this.lastFetch + this.ttl; } /** * Gets the time until cache expires * * @returns Milliseconds until expiry, or 0 if already expired */ getTimeUntilExpiry() { const expiryTime = this.lastFetch + this.ttl; const now = Date.now(); return Math.max(0, expiryTime - now); } /** * Checks if cache is empty (never populated) * * @returns true if cache has never been populated, false otherwise */ isEmpty() { return this.lastFetch === 0; } /** * Manually refreshes the cache * * Fetches fresh data from the API and updates both vaults and assets maps. * Updates lastFetch timestamp on success. * * If a refresh is already in progress, this method waits for it to complete * instead of starting a concurrent refresh. * * @throws {APIError} If the API request fails */ async refresh() { if (this.refreshing) { while (this.refreshing) { await new Promise((resolve) => setTimeout(resolve, 100)); } return; } this.refreshing = true; try { const [vaultsData, assetsData] = await Promise.all([ fetchVaults(), fetchSupportedAssets() ]); this.vaults.clear(); this.assets.clear(); for (const vault of vaultsData) { const tokenAddress = vault.Nucleus.baseTokenAddressId; const existing = this.vaults.get(tokenAddress); if (existing) { existing.push(vault); } else { this.vaults.set(tokenAddress, [vault]); } } for (const asset of assetsData) { this.assets.set(asset.address, asset); } this.lastFetch = Date.now(); } finally { this.refreshing = false; } } /** * Clears the cache * * Removes all cached data and resets lastFetch timestamp. * Does not affect TTL setting. */ clear() { this.vaults.clear(); this.assets.clear(); this.lastFetch = 0; } /** * Gets cache statistics * * @returns Object with cache statistics */ getStats() { return { vaultCount: this.getAllVaults().length, assetCount: this.assets.size, tokenCount: this.vaults.size, lastFetch: this.lastFetch, ttl: this.ttl, isExpired: this.isExpired(), isEmpty: this.isEmpty(), timeUntilExpiry: this.getTimeUntilExpiry() }; } }; // src/api/earn-sdk-client.ts var API_BASE_URL = "http://localhost:8500"; var DEFAULT_TIMEOUT = 1e4; function createTimeoutSignal(timeoutMs) { const controller = new AbortController(); setTimeout(() => controller.abort(), timeoutMs); return controller.signal; } function validateVaultFilterOptions(options) { if (!options) return; if (options.chainId !== void 0 && !isValidChainId(options.chainId)) { throw new APIError( `Invalid chainId: ${options.chainId}. Must be a positive integer.`, { endpoint: "/v1/earn-sdk/vaults" } ); } if (options.yieldType !== void 0 && !isValidYieldType(options.yieldType)) { throw new APIError( `Invalid yieldType: ${options.yieldType}. Must be one of: PRIME, TBILL, LENDING.`, { endpoint: "/v1/earn-sdk/vaults" } ); } if (options.depositTokenAddress !== void 0 && !isValidAddress(options.depositTokenAddress)) { throw new APIError( `Invalid depositTokenAddress: ${options.depositTokenAddress}. Must be a valid Ethereum address.`, { endpoint: "/v1/earn-sdk/vaults" } ); } } function validateAssetFilterOptions(options) { if (!options) return; if (options.chainId !== void 0 && !isValidChainId(options.chainId)) { throw new APIError( `Invalid chainId: ${options.chainId}. Must be a positive integer.`, { endpoint: "/v1/earn-sdk/supported-assets-by-chains" } ); } if (options.address !== void 0 && !isValidAddress(options.address)) { throw new APIError( `Invalid address: ${options.address}. Must be a valid Ethereum address.`, { endpoint: "/v1/earn-sdk/supported-assets-by-chains" } ); } if (options.symbol !== void 0 && typeof options.symbol !== "string") { throw new APIError(`Invalid symbol: ${options.symbol}. Must be a string.`, { endpoint: "/v1/earn-sdk/supported-assets-by-chains" }); } } function validateEarnVault(data) { if (!data || typeof data !== "object") return false; const vault = data; if (typeof vault.id !== "string" || vault.id.length === 0) return false; if (!isValidChainId(vault.chainId)) return false; if (!isValidYieldType(vault.yieldType)) return false; if (!vault.Nucleus || typeof vault.Nucleus !== "object") return false; const nucleus = vault.Nucleus; if (!isValidAddress(nucleus.boringVaultAddressId)) return false; if (!isValidAddress(nucleus.tellerAddressId)) return false; if (!isValidAddress(nucleus.accountantAddressId)) return false; if (!isValidAddress(nucleus.managerAddressId)) return false; if (!isValidAddress(nucleus.rolesAuthorityAddressId)) return false; if (!isValidAddress(nucleus.baseTokenAddressId)) return false; if (nucleus.baseTokenStandInId !== void 0 && !isValidAddress(nucleus.baseTokenStandInId)) { return false; } if (nucleus.communityCodeDepositorAddressId !== void 0 && !isValidAddress(nucleus.communityCodeDepositorAddressId)) { return false; } return true; } function validateSupportedAsset(data) { if (!data || typeof data !== "object") return false; const asset = data; if (!isValidAddress(asset.address)) return false; if (typeof asset.symbol !== "string" || asset.symbol.length === 0) return false; if (typeof asset.name !== "string" || asset.name.length === 0) return false; if (typeof asset.decimals !== "number" || asset.decimals < 0 || !Number.isInteger(asset.decimals)) { return false; } if (!Array.isArray(asset.supportedChains) || asset.supportedChains.length === 0) { return false; } for (const chainId of asset.supportedChains) { if (!isValidChainId(chainId)) return false; } return true; } async function fetchVaults(options) { const endpoint = "/v1/earn-sdk/vaults"; validateVaultFilterOptions(options); const params = new URLSearchParams(); if (options?.chainId !== void 0) { params.append("chainId", options.chainId.toString()); } if (options?.yieldType) { params.append("yieldType", options.yieldType); } if (options?.depositTokenAddress) { params.append("depositTokenAddress", options.depositTokenAddress); } const url = `${API_BASE_URL}${endpoint}${params.toString() ? `?${params.toString()}` : ""}`; try { const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, signal: createTimeoutSignal(DEFAULT_TIMEOUT) }); if (!response.ok) { const errorText = await response.text().catch(() => "Unknown error"); throw new APIError( `Failed to fetch vaults: ${response.status} ${response.statusText}`, { statusCode: response.status, endpoint, cause: errorText } ); } let data; try { data = await response.json(); } catch (error) { throw new APIError("Failed to parse vaults response: Invalid JSON", { statusCode: response.status, endpoint, cause: error }); } if (!Array.isArray(data)) { throw new APIError("Invalid vaults response: Expected array", { statusCode: response.status, endpoint, cause: data }); } const vaults = []; for (let i = 0; i < data.length; i++) { const vault = data[i]; if (!validateEarnVault(vault)) { throw new APIError( `Invalid vault data at index ${i}: Failed validation`, { statusCode: response.status, endpoint, cause: vault } ); } vaults.push(vault); } return vaults; } catch (error) { if (error instanceof Error && error.name === "AbortError") { throw new APIError(`Failed to fetch vaults: Network timeout`, { endpoint, cause: error }); } if (error instanceof APIError) { throw error; } if (error instanceof TypeError) { throw new APIError(`Failed to fetch vaults: Network error`, { endpoint, cause: error }); } throw new APIError(`Failed to fetch vaults: Unknown error`, { endpoint, cause: error }); } } async function fetchSupportedAssets(options) { const endpoint = "/v1/earn-sdk/supported-assets-by-chains"; validateAssetFilterOptions(options); const params = new URLSearchParams(); if (options?.chainId !== void 0) { params.append("chainId", options.chainId.toString()); } if (options?.symbol) { params.append("symbol", options.symbol); } if (options?.address) { params.append("address", options.address); } const url = `${API_BASE_URL}${endpoint}${params.toString() ? `?${params.toString()}` : ""}`; try { const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, signal: createTimeoutSignal(DEFAULT_TIMEOUT) }); if (!response.ok) { const errorText = await response.text().catch(() => "Unknown error"); throw new APIError( `Failed to fetch supported assets: ${response.status} ${response.statusText}`, { statusCode: response.status, endpoint, cause: errorText } ); } let data; try { data = await response.json(); } catch (error) { throw new APIError( "Failed to parse supported assets response: Invalid JSON", { statusCode: response.status, endpoint, cause: error } ); } if (!Array.isArray(data)) { throw new APIError("Invalid supported assets response: Expected array", { statusCode: response.status, endpoint, cause: data }); } const assets = []; for (let i = 0; i < data.length; i++) { const asset = data[i]; if (!validateSupportedAsset(asset)) { throw new APIError( `Invalid asset data at index ${i}: Failed validation`, { statusCode: response.status, endpoint, cause: asset } ); } assets.push(asset); } return assets; } catch (error) { if (error instanceof Error && error.name === "AbortError") { throw new APIError(`Failed to fetch supported assets: Network timeout`, { endpoint, cause: error }); } if (error instanceof APIError) { throw error; } if (error instanceof TypeError) { throw new APIError(`Failed to fetch supported assets: Network error`, { endpoint, cause: error }); } throw new APIError(`Failed to fetch supported assets: Unknown error`, { endpoint, cause: error }); } } var globalCache = null; function initializeCache(ttl) { globalCache = new VaultCache(ttl); return globalCache; } function getCache() { if (!globalCache) { globalCache = new VaultCache(); } return globalCache; } async function refreshVaultCache() { const cache = getCache(); await cache.refresh(); } async function getVaultsFromCache(options) { const cache = getCache(); if (cache.isEmpty() || cache.isExpired()) { await cache.refresh(); } let vaults = cache.getAllVaults(); if (options?.chainId !== void 0) { vaults = vaults.filter((v) => v.chainId === options.chainId); } if (options?.yieldType) { vaults = vaults.filter((v) => v.yieldType === options.yieldType); } if (options?.depositTokenAddress) { vaults = vaults.filter( (v) => v.Nucleus.baseTokenAddressId === options.depositTokenAddress ); } return vaults; } async function getAssetsFromCache(options) { const cache = getCache(); if (cache.isEmpty() || cache.isExpired()) { await cache.refresh(); } let assets = cache.getAllAssets(); if (options?.chainId !== void 0) { const chainId = options.chainId; assets = assets.filter((a) => a.supportedChains.includes(chainId)); } if (options?.symbol) { assets = assets.filter((a) => a.symbol === options.symbol); } if (options?.address) { assets = assets.filter((a) => a.address === options.address); } return assets; } async function findVaultByConfig(params) { if (!isValidAddress(params.depositTokenAddress)) { throw new APIError( `Invalid depositTokenAddress: ${params.depositTokenAddress}. Must be a valid Ethereum address.`, { endpoint: "findVaultByConfig" } ); } if (!isValidYieldType(params.yieldType)) { throw new APIError( `Invalid yieldType: ${params.yieldType}. Must be one of: PRIME, TBILL, LENDING.`, { endpoint: "findVaultByConfig" } ); } if (!isValidChainId(params.chainId)) { throw new APIError( `Invalid chainId: ${params.chainId}. Must be a positive integer.`, { endpoint: "findVaultByConfig" } ); } const cache = getCache(); if (cache.isExpired()) { await cache.refresh(); } const normalizedAddress = params.depositTokenAddress.toLowerCase(); let vaultsByToken = cache.getVault(params.depositTokenAddress); if (!vaultsByToken) { vaultsByToken = cache.getVault(normalizedAddress); } if (!vaultsByToken) { const allVaults = cache.getAllVaults(); const matchingVaults = allVaults.filter( (vault) => vault.Nucleus.baseTokenAddressId.toLowerCase() === normalizedAddress ); vaultsByToken = matchingVaults.length > 0 ? matchingVaults : void 0; } if (!vaultsByToken || vaultsByToken.length === 0) { return null; } const matchingVault = vaultsByToken.find( (vault) => vault.yieldType === params.yieldType && vault.chainId === params.chainId ); return matchingVault || null; } async function getWithdrawSupportedAssets() { const cache = getCache(); if (cache.isEmpty() || cache.isExpired()) { await cache.refresh(); } const vaults = cache.getAllVaults(); const assets = cache.getAllAssets(); const result = []; const assetMap = /* @__PURE__ */ new Map(); for (const asset of assets) { assetMap.set(asset.address.toLowerCase(), asset); } const assetVaultMap = /* @__PURE__ */ new Map(); for (const vault of vaults) { const assetAddress = vault.Nucleus.baseTokenAddressId.toLowerCase(); if (!assetVaultMap.has(assetAddress)) { assetVaultMap.set(assetAddress, []); } assetVaultMap.get(assetAddress)?.push({ id: vault.id, yieldType: vault.yieldType, chainId: vault.chainId, vaultId: vault.id }); } for (const [assetAddress, vaultsData] of assetVaultMap.entries()) { const asset = assetMap.get(assetAddress); if (asset) { result.push({ address: asset.address, symbol: asset.symbol, decimals: asset.decimals, vaults: vaultsData }); } } return result; } var rari = defineChain({ id: 1380012617, name: "Rari Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://mainnet.rpc.rarichain.org/http"], webSocket: ["wss://mainnet.rpc.rarichain.org/ws"] } }, blockExplorers: { default: { name: "Rari Explorer", url: "https://mainnet.explorer.rarichain.org" } }, contracts: { multicall3: { address: "0x3F5Fc48153f8aDd3E429F0c84fA6FEd5c58657Dc" } } }); var hyperEvm = defineChain({ id: 999, name: "HyperEVM", nativeCurrency: { decimals: 18, name: "Hyperliquid", symbol: "HYPE" }, rpcUrls: { default: { http: ["https://rpc.hyperliquid.xyz/evm"], webSocket: ["wss://hyperliquid.drpc.org"] } }, blockExplorers: { default: { name: "Explorer", url: "https://purrsec.com/" } }, contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11", blockCreated: 13051 } } }); var CHAIN_ID_TO_CHAIN_MAP = { [mainnet.id]: mainnet, [boba.id]: boba, [form.id]: form, [hyperEvm.id]: hyperEvm, [plumeMainnet.id]: plumeMainnet, [rari.id]: rari, [sei.id]: sei, [swellchain.id]: swellchain }; // src/utils/chain-utils.ts var chainsCache = null; async function getChainFromConfig(chainId, config) { if (chainsCache && !config) { const chain2 = chainsCache.get(Number(chainId)); if (chain2) return chain2; } const vaults = config ?? await fetchVaults(); const vault = vaults.find((v) => v.chainId === chainId); if (!vault) { throw new Error(`Vault not found for ID: ${chainId}`); } if (config && !chainsCache) { const cache = /* @__PURE__ */ new Map(); for (const v of vaults) { if (v.chainId === chainId) { cache.set(v.chainId, { id: v.chainId, name: CHAIN_ID_TO_CHAIN_MAP[v.chainId]?.name, nativeCurrency: CHAIN_ID_TO_CHAIN_MAP[v.chainId]?.nativeCurrency, rpcUrls: CHAIN_ID_TO_CHAIN_MAP[v.chainId]?.rpcUrls }); } } chainsCache = cache; } const chain = chainsCache ? chainsCache.get(chainId) : void 0; if (!chain) { throw new Error(`Chain not found for ID: ${chainId}`); } return chain; } function clearChainsCache() { chainsCache = null; } // src/constants/index.ts var ATOMIC_QUEUE_CONTRACT_ADDRESS = "0x228c44bb4885c6633f4b6c83f14622f37d5112e5"; var DEFAULT_WITHDRAW_SLIPPAGE = 20; var DEFAULT_DEPOSIT_SLIPPAGE = 50; var DEFAULT_DEADLINE = 3; var NATIVE_TOKEN_FOR_BRIDGE_FEE = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; var CHAINLINK_ADDRESS = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"; var DEFAULT_APPROVAL_AMOUNT = BigInt(2) ** BigInt(256) - BigInt(1); var NUCLEUS_BASE_URL = "https://backend.nucleusearn.io/v1/protocol"; export { APIError, ATOMIC_QUEUE_CONTRACT_ADDRESS, CHAINLINK_ADDRESS, DEFAULT_APPROVAL_AMOUNT, DEFAULT_DEADLINE, DEFAULT_DEPOSIT_SLIPPAGE, DEFAULT_WITHDRAW_SLIPPAGE, NATIVE_TOKEN_FOR_BRIDGE_FEE, NUCLEUS_BASE_URL, clearChainsCache, fetchSupportedAssets, fetchVaults, findVaultByConfig, getAssetsFromCache, getCache, getChainFromConfig, getVaultsFromCache, getWithdrawSupportedAssets, initializeCache, refreshVaultCache }; //# sourceMappingURL=chunk-NTRZGVUA.mjs.map //# sourceMappingURL=chunk-NTRZGVUA.mjs.map