@paxoslabs/earn-sdk
Version:
Paxos Labs Earn SDK
703 lines (697 loc) • 21.4 kB
JavaScript
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