UNPKG

@solana-tracker/data-api

Version:

Official Solana Tracker Data API client for accessing Solana Data

1,405 lines (1,381 loc) 56.2 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/event-processor.ts var timeframes = { "1m": { time: 60 }, "5m": { time: 300 }, "15m": { time: 900 }, "30m": { time: 1800 }, "1h": { time: 3600 }, "2h": { time: 7200 }, "3h": { time: 10800 }, "4h": { time: 14400 }, "5h": { time: 18e3 }, "6h": { time: 21600 }, "12h": { time: 43200 }, "24h": { time: 86400 } }; var timeframeBoundaries = Object.entries(timeframes).map(([key, value]) => ({ key, seconds: value.time })).sort((a, b) => a.seconds - b.seconds); function decodeBinaryEvents(binaryData) { const data = binaryData instanceof ArrayBuffer ? new Uint8Array(binaryData) : binaryData; const view = new DataView(data.buffer, data.byteOffset, data.byteLength); let offset = 0; const walletCount = view.getUint32(offset, true); offset += 4; const wallets = []; for (let i = 0; i < walletCount; i++) { const length = view.getUint8(offset++); const walletBytes = data.slice(offset, offset + length); const wallet = new TextDecoder().decode(walletBytes); wallets.push(wallet); offset += length; } const tradeCount = view.getUint32(offset, true); offset += 4; const events = []; for (let i = 0; i < tradeCount; i++) { const walletIndex = view.getUint32(offset, true); const amount = view.getFloat32(offset + 4, true); const priceUsd = view.getFloat32(offset + 8, true); const volume = view.getFloat32(offset + 12, true); const typeCode = view.getUint8(offset + 16); const timeInSeconds = view.getUint32(offset + 17, true); events.push({ wallet: wallets[walletIndex], amount, priceUsd, volume, type: typeCode === 0 ? "buy" : "sell", time: timeInSeconds * 1e3 }); offset += 21; } return events; } function processEvents(binaryData) { let events; if (binaryData instanceof ArrayBuffer || binaryData instanceof Uint8Array) { events = decodeBinaryEvents(binaryData); } else if (Array.isArray(binaryData)) { events = binaryData; } else { throw new Error("Invalid input: expected binary data or events array"); } if (events.length === 0) return {}; const currentPrice = parseFloat(events[0].priceUsd.toString()); const currentTimestamp = Date.now() / 1e3; const stats = {}; timeframeBoundaries.forEach(({ key }) => { stats[key] = { buys: 0, sells: 0, buyVolume: 0, sellVolume: 0, buyers: /* @__PURE__ */ new Map(), sellers: /* @__PURE__ */ new Map(), totalTransactions: 0, totalVolume: 0, totalWallets: /* @__PURE__ */ new Map(), initialPrice: 0, lastPrice: 0, hasData: false }; }); for (let i = 0; i < events.length; i++) { const event = events[i]; const { time: timestamp, type, volume, wallet, priceUsd } = event; if (type !== "buy" && type !== "sell") continue; const eventTime = timestamp / 1e3; const timeDiff = currentTimestamp - eventTime; for (let j = 0; j < timeframeBoundaries.length; j++) { const { key, seconds } = timeframeBoundaries[j]; if (timeDiff > seconds) continue; const period = stats[key]; if (!period.hasData) { period.initialPrice = parseFloat(priceUsd.toString()); period.hasData = true; } period.totalTransactions++; period.totalVolume += parseFloat(volume.toString() || "0"); period.totalWallets.set(wallet, true); period.lastPrice = parseFloat(priceUsd.toString()); if (type === "buy") { period.buys++; period.buyVolume += parseFloat(volume.toString() || "0"); period.buyers.set(wallet, true); } else { period.sells++; period.sellVolume += parseFloat(volume.toString() || "0"); period.sellers.set(wallet, true); } } } const sortedStats = {}; Object.keys(timeframes).forEach((timeframe) => { const period = stats[timeframe]; if (!period.hasData) return; const priceChangePercent = period.initialPrice > 0 ? 100 * ((currentPrice - period.lastPrice) / period.lastPrice) : 0; const timeframeStats = { buyers: period.buyers.size, sellers: period.sellers.size, volume: { buys: period.buyVolume, sells: period.sellVolume, total: period.totalVolume }, transactions: period.totalTransactions, buys: period.buys, sells: period.sells, wallets: period.totalWallets.size, price: period.initialPrice, priceChangePercentage: priceChangePercent }; sortedStats[timeframe] = timeframeStats; }); return sortedStats; } async function processEventsAsync(binaryData, onProgress) { let events; if (binaryData instanceof ArrayBuffer || binaryData instanceof Uint8Array) { events = decodeBinaryEvents(binaryData); } else if (Array.isArray(binaryData)) { events = binaryData; } else { throw new Error("Invalid input: expected binary data or events array"); } if (events.length === 0) return {}; const CHUNK_SIZE = 1e5; const currentPrice = parseFloat(events[0].priceUsd.toString()); const currentTimestamp = Date.now() / 1e3; const stats = {}; timeframeBoundaries.forEach(({ key }) => { stats[key] = { buys: 0, sells: 0, buyVolume: 0, sellVolume: 0, buyers: /* @__PURE__ */ new Map(), sellers: /* @__PURE__ */ new Map(), totalTransactions: 0, totalVolume: 0, totalWallets: /* @__PURE__ */ new Map(), initialPrice: 0, lastPrice: 0, hasData: false }; }); for (let chunk = 0; chunk < events.length; chunk += CHUNK_SIZE) { await new Promise((resolve) => { setTimeout(() => { const end = Math.min(chunk + CHUNK_SIZE, events.length); for (let i = chunk; i < end; i++) { const event = events[i]; const { time: timestamp, type, volume, wallet, priceUsd } = event; if (type !== "buy" && type !== "sell") continue; const eventTime = timestamp / 1e3; const timeDiff = currentTimestamp - eventTime; for (let j = 0; j < timeframeBoundaries.length; j++) { const { key, seconds } = timeframeBoundaries[j]; if (timeDiff > seconds) continue; const period = stats[key]; if (!period.hasData) { period.initialPrice = parseFloat(priceUsd.toString()); period.hasData = true; } period.totalTransactions++; period.totalVolume += parseFloat(volume.toString() || "0"); period.totalWallets.set(wallet, true); period.lastPrice = parseFloat(priceUsd.toString()); if (type === "buy") { period.buys++; period.buyVolume += parseFloat(volume.toString() || "0"); period.buyers.set(wallet, true); } else { period.sells++; period.sellVolume += parseFloat(volume.toString() || "0"); period.sellers.set(wallet, true); } } } if (onProgress) { onProgress(end / events.length * 100); } resolve(); }, 0); }); } const sortedStats = {}; Object.keys(timeframes).forEach((timeframe) => { const period = stats[timeframe]; if (!period.hasData) return; const priceChangePercent = period.initialPrice > 0 ? 100 * ((currentPrice - period.lastPrice) / period.lastPrice) : 0; const timeframeStats = { buyers: period.buyers.size, sellers: period.sellers.size, volume: { buys: period.buyVolume, sells: period.sellVolume, total: period.totalVolume }, transactions: period.totalTransactions, buys: period.buys, sells: period.sells, wallets: period.totalWallets.size, price: period.initialPrice, priceChangePercentage: priceChangePercent }; sortedStats[timeframe] = timeframeStats; }); return sortedStats; } // src/data-api.ts var DataApiError = class extends Error { constructor(message, status, code) { super(message); this.status = status; this.code = code; this.name = "DataApiError"; } }; var RateLimitError = class extends DataApiError { constructor(message, retryAfter) { super(message, 429, "RATE_LIMIT_EXCEEDED"); this.retryAfter = retryAfter; this.name = "RateLimitError"; } }; var ValidationError = class extends DataApiError { constructor(message) { super(message, 400, "VALIDATION_ERROR"); this.name = "ValidationError"; } }; var Client = class { /** * Creates a new instance of the Solana Tracker Data API client * @param config Configuration options including API key */ constructor(config) { this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || "https://data.solanatracker.io"; } /** * Makes a request to the API * @param endpoint The API endpoint * @param options Additional fetch options * @returns The API response */ async request(endpoint, options) { const headers = { "x-api-key": this.apiKey, "Content-Type": "application/json", ...options == null ? void 0 : options.headers }; try { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers }); if (!response.ok) { if (response.status === 429) { const retryAfter = response.headers.get("Retry-After"); if (options == null ? void 0 : options.disableLogs) { console.warn(`Rate limit exceeded for ${endpoint}. Retry after: ${retryAfter || "1"} seconds`); } throw new RateLimitError( "Rate limit exceeded", retryAfter ? parseInt(retryAfter) : void 0 ); } throw new DataApiError( `API request failed: ${response.status} ${response.statusText}`, response.status ); } return response.json(); } catch (error) { if (error instanceof DataApiError) { throw error; } throw new DataApiError("An unexpected error occurred"); } } /** * Validates a Solana public key * @param address The address to validate * @param paramName The parameter name for error messaging * @throws ValidationError if the address is invalid */ validatePublicKey(address, paramName) { if (!address || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) { throw new ValidationError(`Invalid ${paramName}: ${address}`); } } // ======== TOKEN ENDPOINTS ======== /** * Get comprehensive information about a specific token * @param tokenAddress The token's mint address * @returns Detailed token information */ async getTokenInfo(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/tokens/${tokenAddress}`); } /** * Get token information by searching with a pool address * @param poolAddress The pool address * @returns Detailed token information */ async getTokenByPool(poolAddress) { this.validatePublicKey(poolAddress, "poolAddress"); return this.request(`/tokens/by-pool/${poolAddress}`); } /** * Get token holders information * @param tokenAddress The token's mint address * @returns Information about token holders */ async getTokenHolders(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/tokens/${tokenAddress}/holders`); } /** * Get top 20 token holders * @param tokenAddress The token's mint address * @returns Top holders information */ async getTopHolders(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/tokens/${tokenAddress}/holders/top`); } /** * Get the all-time high price for a token * @param tokenAddress The token's mint address * @returns All-time high price data */ async getAthPrice(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/tokens/${tokenAddress}/ath`); } /** * Get tokens created by a specific wallet with pagination * @param wallet The deployer wallet address * @param page Page number (default: 1) * @param limit Number of items per page (default: 250, max: 500) * @returns List of tokens created by the deployer */ async getTokensByDeployer(wallet, page, limit) { this.validatePublicKey(wallet, "wallet"); const params = new URLSearchParams(); if (page) params.append("page", page.toString()); if (limit) params.append("limit", limit.toString()); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/deployer/${wallet}${query}`); } /** * Search for tokens with flexible filtering options * @param params Search parameters and filters * @returns Search results */ async searchTokens(params) { const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== void 0) { queryParams.append(key, value.toString()); } } return this.request(`/search?${queryParams}`); } /** * Get the latest tokens * @param page Page number (1-10) * @returns List of latest tokens */ async getLatestTokens(page = 1) { if (page < 1 || page > 10) { throw new ValidationError("Page must be between 1 and 10"); } return this.request(`/tokens/latest?page=${page}`); } /** * Get information about multiple tokens * @param tokenAddresses Array of token addresses * @returns Information about multiple tokens */ async getMultipleTokens(tokenAddresses) { if (tokenAddresses.length > 20) { throw new ValidationError("Maximum of 20 tokens per request"); } tokenAddresses.forEach((addr) => this.validatePublicKey(addr, "tokenAddress")); return this.request("/tokens/multi", { method: "POST", body: JSON.stringify({ tokens: tokenAddresses }) }); } /** * Get trending tokens * @param timeframe Optional timeframe for trending calculation * @returns List of trending tokens */ async getTrendingTokens(timeframe) { const validTimeframes = ["5m", "15m", "30m", "1h", "2h", "3h", "4h", "5h", "6h", "12h", "24h"]; if (timeframe && !validTimeframes.includes(timeframe)) { throw new ValidationError(`Invalid timeframe. Must be one of: ${validTimeframes.join(", ")}`); } const endpoint = timeframe ? `/tokens/trending/${timeframe}` : "/tokens/trending"; return this.request(endpoint); } /** * Get tokens sorted by volume * @param timeframe Optional timeframe for volume calculation * @returns List of tokens sorted by volume */ async getTokensByVolume(timeframe) { const validTimeframes = ["5m", "15m", "30m", "1h", "6h", "12h", "24h"]; if (timeframe && !validTimeframes.includes(timeframe)) { throw new ValidationError(`Invalid timeframe. Must be one of: ${validTimeframes.join(", ")}`); } const endpoint = timeframe ? `/tokens/volume/${timeframe}` : "/tokens/volume"; return this.request(endpoint); } /** * Get an overview of latest, graduating, and graduated tokens * @param limit Optional limit for the number of tokens per category * @returns Token overview (Memescope / Pumpvision style) */ async getTokenOverview(limit) { if (limit !== void 0 && (!Number.isInteger(limit) || limit <= 0)) { throw new ValidationError("Limit must be a positive integer"); } const endpoint = limit ? `/tokens/multi/all?limit=${limit}` : "/tokens/multi/all"; return this.request(endpoint); } /** * Get graduated tokens * @returns List of graduated tokens */ async getGraduatedTokens() { return this.request("/tokens/multi/graduated"); } // ======== PRICE ENDPOINTS ======== /** * Get price information for a token * @param tokenAddress The token's mint address * @param priceChanges Include price change percentages * @returns Price data */ async getPrice(tokenAddress, priceChanges) { this.validatePublicKey(tokenAddress, "tokenAddress"); const query = priceChanges ? "&priceChanges=true" : ""; return this.request(`/price?token=${tokenAddress}${query}`); } /** * Get historic price information for a token * @param tokenAddress The token's mint address * @returns Historic price data */ async getPriceHistory(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/price/history?token=${tokenAddress}`); } /** * Get price at a specific timestamp * @param tokenAddress The token's mint address * @param timestamp Unix timestamp * @returns Price at the specified timestamp */ async getPriceAtTimestamp(tokenAddress, timestamp) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/price/history/timestamp?token=${tokenAddress}&timestamp=${timestamp}`); } /** * Get lowest and highest price in a time range * @param tokenAddress The token's mint address * @param timeFrom Start time (unix timestamp) * @param timeTo End time (unix timestamp) * @returns Price range data */ async getPriceRange(tokenAddress, timeFrom, timeTo) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/price/history/range?token=${tokenAddress}&time_from=${timeFrom}&time_to=${timeTo}`); } /** * Get price information for a token (POST method) * @param tokenAddress The token's mint address * @param priceChanges Include price change percentages * @returns Price data */ async postPrice(tokenAddress, priceChanges) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request("/price", { method: "POST", body: JSON.stringify({ token: tokenAddress, priceChanges: priceChanges || false }) }); } /** * Get price information for multiple tokens * @param tokenAddresses Array of token addresses * @param priceChanges Include price change percentages * @returns Price data for multiple tokens */ async getMultiplePrices(tokenAddresses, priceChanges) { if (tokenAddresses.length > 100) { throw new ValidationError("Maximum of 100 tokens per request"); } tokenAddresses.forEach((addr) => this.validatePublicKey(addr, "tokenAddress")); const query = priceChanges ? "&priceChanges=true" : ""; return this.request(`/price/multi?tokens=${tokenAddresses.join(",")}${query}`); } /** * Get price information for multiple tokens (POST method) * @param tokenAddresses Array of token addresses * @param priceChanges Include price change percentages * @returns Price data for multiple tokens */ async postMultiplePrices(tokenAddresses, priceChanges) { if (tokenAddresses.length > 100) { throw new ValidationError("Maximum of 100 tokens per request"); } tokenAddresses.forEach((addr) => this.validatePublicKey(addr, "tokenAddress")); return this.request("/price/multi", { method: "POST", body: JSON.stringify({ tokens: tokenAddresses, priceChanges: priceChanges || false }) }); } // ======== WALLET ENDPOINTS ======== /** * Get basic wallet information * @param owner Wallet address * @returns Basic wallet data */ async getWalletBasic(owner) { this.validatePublicKey(owner, "owner"); return this.request(`/wallet/${owner}/basic`); } /** * Get all tokens in a wallet * @param owner Wallet address * @returns Detailed wallet data */ async getWallet(owner) { this.validatePublicKey(owner, "owner"); return this.request(`/wallet/${owner}`); } /** * Get wallet tokens with pagination * @param owner Wallet address * @param page Page number * @returns Paginated wallet data */ async getWalletPage(owner, page) { this.validatePublicKey(owner, "owner"); return this.request(`/wallet/${owner}/page/${page}`); } /** * Get wallet portfolio chart data with PnL information * @param wallet Wallet address * @returns Wallet chart data with historical values and PnL * @throws DataApiError if no data found for the wallet */ async getWalletChart(wallet) { this.validatePublicKey(wallet, "wallet"); return this.request(`/wallet/${wallet}/chart`); } /** * Get wallet trades * @param owner Wallet address * @param cursor Pagination cursor * @param showMeta Include token metadata * @param parseJupiter Parse Jupiter swaps * @param hideArb Hide arbitrage transactions * @returns Wallet trades data */ async getWalletTrades(owner, cursor, showMeta, parseJupiter, hideArb) { this.validatePublicKey(owner, "owner"); const params = new URLSearchParams(); if (cursor) params.append("cursor", cursor.toString()); if (showMeta) params.append("showMeta", "true"); if (parseJupiter) params.append("parseJupiter", "true"); if (hideArb) params.append("hideArb", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/wallet/${owner}/trades${query}`); } // ======== TRADE ENDPOINTS ======== /** * Get trades for a token * @param tokenAddress Token address * @param cursor Pagination cursor * @param showMeta Include token metadata * @param parseJupiter Parse Jupiter swaps * @param hideArb Hide arbitrage transactions * @returns Token trades data */ async getTokenTrades(tokenAddress, cursor, showMeta, parseJupiter, hideArb) { this.validatePublicKey(tokenAddress, "tokenAddress"); const params = new URLSearchParams(); if (cursor) params.append("cursor", cursor.toString()); if (showMeta) params.append("showMeta", "true"); if (parseJupiter) params.append("parseJupiter", "true"); if (hideArb) params.append("hideArb", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/trades/${tokenAddress}${query}`); } /** * Get trades for a specific token and pool * @param tokenAddress Token address * @param poolAddress Pool address * @param cursor Pagination cursor * @param showMeta Include token metadata * @param parseJupiter Parse Jupiter swaps * @param hideArb Hide arbitrage transactions * @returns Pool-specific token trades data */ async getPoolTrades(tokenAddress, poolAddress, cursor, showMeta, parseJupiter, hideArb) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(poolAddress, "poolAddress"); const params = new URLSearchParams(); if (cursor) params.append("cursor", cursor.toString()); if (showMeta) params.append("showMeta", "true"); if (parseJupiter) params.append("parseJupiter", "true"); if (hideArb) params.append("hideArb", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/trades/${tokenAddress}/${poolAddress}${query}`); } /** * Get trades for a specific token, pool, and wallet * @param tokenAddress Token address * @param poolAddress Pool address * @param owner Wallet address * @param cursor Pagination cursor * @param showMeta Include token metadata * @param parseJupiter Parse Jupiter swaps * @param hideArb Hide arbitrage transactions * @returns User-specific pool trades data */ async getUserPoolTrades(tokenAddress, poolAddress, owner, cursor, showMeta, parseJupiter, hideArb) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(poolAddress, "poolAddress"); this.validatePublicKey(owner, "owner"); const params = new URLSearchParams(); if (cursor) params.append("cursor", cursor.toString()); if (showMeta) params.append("showMeta", "true"); if (parseJupiter) params.append("parseJupiter", "true"); if (hideArb) params.append("hideArb", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/trades/${tokenAddress}/${poolAddress}/${owner}${query}`); } /** * Get trades for a specific token and wallet * @param tokenAddress Token address * @param owner Wallet address * @param cursor Pagination cursor * @param showMeta Include token metadata * @param parseJupiter Parse Jupiter swaps * @param hideArb Hide arbitrage transactions * @returns User-specific token trades data */ async getUserTokenTrades(tokenAddress, owner, cursor, showMeta, parseJupiter, hideArb) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(owner, "owner"); const params = new URLSearchParams(); if (cursor) params.append("cursor", cursor.toString()); if (showMeta) params.append("showMeta", "true"); if (parseJupiter) params.append("parseJupiter", "true"); if (hideArb) params.append("hideArb", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/trades/${tokenAddress}/by-wallet/${owner}${query}`); } // ======== CHART DATA ENDPOINTS ======== /** * Get OHLCV data for a token * @param tokenAddress Token address * @param type Time interval (e.g., "1s", "1m", "1h", "1d") * @param timeFrom Start time (Unix timestamp in seconds) * @param timeTo End time (Unix timestamp in seconds) * @param marketCap Return chart for market cap instead of pricing * @param removeOutliers Disable outlier removal if set to false (default: true) * @returns OHLCV chart data */ async getChartData(tokenAddress, type, timeFrom, timeTo, marketCap, removeOutliers) { this.validatePublicKey(tokenAddress, "tokenAddress"); const params = new URLSearchParams(); if (type) params.append("type", type); if (timeFrom) params.append("time_from", timeFrom.toString()); if (timeTo) params.append("time_to", timeTo.toString()); if (marketCap) params.append("marketCap", "true"); if (removeOutliers === false) params.append("removeOutliers", "false"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/chart/${tokenAddress}${query}`); } /** * Get OHLCV data for a specific token and pool * @param tokenAddress Token address * @param poolAddress Pool address * @param type Time interval (e.g., "1s", "1m", "1h", "1d") * @param timeFrom Start time (Unix timestamp in seconds) * @param timeTo End time (Unix timestamp in seconds) * @param marketCap Return chart for market cap instead of pricing * @param removeOutliers Disable outlier removal if set to false (default: true) * @returns OHLCV chart data for a specific pool */ async getPoolChartData(tokenAddress, poolAddress, type, timeFrom, timeTo, marketCap, removeOutliers) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(poolAddress, "poolAddress"); const params = new URLSearchParams(); if (type) params.append("type", type); if (timeFrom) params.append("time_from", timeFrom.toString()); if (timeTo) params.append("time_to", timeTo.toString()); if (marketCap) params.append("marketCap", "true"); if (removeOutliers === false) params.append("removeOutliers", "false"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/chart/${tokenAddress}/${poolAddress}${query}`); } /** * Get holder count chart data * @param tokenAddress Token address * @param type Time interval (e.g., "1s", "1m", "1h", "1d") * @param timeFrom Start time (Unix timestamp in seconds) * @param timeTo End time (Unix timestamp in seconds) * @returns Holder count chart data */ async getHoldersChart(tokenAddress, type, timeFrom, timeTo) { this.validatePublicKey(tokenAddress, "tokenAddress"); const params = new URLSearchParams(); if (type) params.append("type", type); if (timeFrom) params.append("time_from", timeFrom.toString()); if (timeTo) params.append("time_to", timeTo.toString()); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/holders/chart/${tokenAddress}${query}`); } // ======== PNL DATA ENDPOINTS ======== /** * Get PnL data for all positions of a wallet * @param wallet Wallet address * @param showHistoricPnL Add PnL data for 1d, 7d and 30d intervals (BETA) * @param holdingCheck Additional check for current holding value * @param hideDetails Return only summary without data for each token * @returns Wallet PnL data */ async getWalletPnL(wallet, showHistoricPnL, holdingCheck, hideDetails) { this.validatePublicKey(wallet, "wallet"); const params = new URLSearchParams(); if (showHistoricPnL) params.append("showHistoricPnL", "true"); if (holdingCheck) params.append("holdingCheck", "true"); if (hideDetails) params.append("hideDetails", "true"); const query = params.toString() ? `?${params.toString()}` : ""; return this.request(`/pnl/${wallet}${query}`); } /** * Get the first 100 buyers of a token with PnL data * @param tokenAddress Token address * @returns First buyers data with PnL */ async getFirstBuyers(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/first-buyers/${tokenAddress}`); } /** * Get PnL data for a specific token in a wallet * @param wallet Wallet address * @param tokenAddress Token address * @returns Token-specific PnL data */ async getTokenPnL(wallet, tokenAddress) { this.validatePublicKey(wallet, "wallet"); this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/pnl/${wallet}/${tokenAddress}`); } // ======== TOP TRADERS ENDPOINTS ======== /** * Get the most profitable traders across all tokens * @param page Page number (optional) * @param expandPnL Include detailed PnL data for each token * @param sortBy Sort results by metric ("total" or "winPercentage") * @returns Top traders data */ async getTopTraders(page, expandPnL, sortBy) { const params = new URLSearchParams(); if (expandPnL) params.append("expandPnL", "true"); if (sortBy) params.append("sortBy", sortBy); const query = params.toString() ? `?${params.toString()}` : ""; const endpoint = page ? `/top-traders/all/${page}${query}` : `/top-traders/all${query}`; return this.request(endpoint); } /** * Get top 100 traders by PnL for a token * @param tokenAddress Token address * @returns Top traders for a specific token */ async getTokenTopTraders(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/top-traders/${tokenAddress}`); } // ======== ADDITIONAL ENDPOINTS ======== /** * Get detailed stats for a token over various time intervals * @param tokenAddress Token address * @returns Detailed token stats */ async getTokenStats(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); return this.request(`/stats/${tokenAddress}`); } /** * Get detailed stats for a specific token and pool * @param tokenAddress Token address * @param poolAddress Pool address * @returns Detailed token-pool stats */ async getPoolStats(tokenAddress, poolAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(poolAddress, "poolAddress"); return this.request(`/stats/${tokenAddress}/${poolAddress}`); } /** * Get remaining API credits for the current API key * @returns Credits information */ async getCredits() { return this.request("/credits"); } /** * Get events data for a token (all pools) * NOTE: For non-live statistics, use getTokenStats() instead which is more efficient * @param tokenAddress The token's mint address * @returns Decoded events array */ async getEvents(tokenAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); const response = await fetch(`${this.baseUrl}/events/${tokenAddress}`, { headers: { "x-api-key": this.apiKey, "Accept": "application/octet-stream" } }); if (!response.ok) { if (response.status === 429) { const retryAfter = response.headers.get("Retry-After"); throw new RateLimitError( "Rate limit exceeded", retryAfter ? parseInt(retryAfter) : void 0 ); } throw new DataApiError( `API request failed: ${response.status} ${response.statusText}`, response.status ); } const binaryData = await response.arrayBuffer(); const events = decodeBinaryEvents(binaryData); return events; } /** * Get events data for a specific token and pool * NOTE: For non-live statistics, use getPoolStats() instead which is more efficient * @param tokenAddress The token's mint address * @param poolAddress The pool's address * @returns Decoded events array */ async getPoolEvents(tokenAddress, poolAddress) { this.validatePublicKey(tokenAddress, "tokenAddress"); this.validatePublicKey(poolAddress, "poolAddress"); const response = await fetch(`${this.baseUrl}/events/${tokenAddress}/${poolAddress}`, { headers: { "x-api-key": this.apiKey, "Accept": "application/octet-stream" } }); if (!response.ok) { if (response.status === 429) { const retryAfter = response.headers.get("Retry-After"); throw new RateLimitError( "Rate limit exceeded", retryAfter ? parseInt(retryAfter) : void 0 ); } throw new DataApiError( `API request failed: ${response.status} ${response.statusText}`, response.status ); } const binaryData = await response.arrayBuffer(); const events = decodeBinaryEvents(binaryData); return events; } }; // src/datastream.ts import { EventEmitter } from "events"; // src/websocket-polyfill.ts var isBrowser = typeof window !== "undefined" && typeof window.WebSocket !== "undefined"; if (!isBrowser) { try { const WebSocketImpl = __require("ws"); if (typeof global !== "undefined" && !global.WebSocket) { global.WebSocket = WebSocketImpl; } } catch (e) { console.warn( 'WebSocket implementation not found. If you are using Node.js, please install the "ws" package: npm install ws' ); } } // src/datastream.ts var DatastreamRoom = /* @__PURE__ */ ((DatastreamRoom2) => { DatastreamRoom2["LATEST"] = "latest"; DatastreamRoom2["PRICE_BY_TOKEN"] = "price-by-token"; DatastreamRoom2["PRICE_BY_POOL"] = "price"; DatastreamRoom2["TOKEN_TRANSACTIONS"] = "transaction"; DatastreamRoom2["WALLET_TRANSACTIONS"] = "wallet"; DatastreamRoom2["GRADUATING"] = "graduating"; DatastreamRoom2["GRADUATED"] = "graduated"; DatastreamRoom2["CURVE_PERCENTAGE"] = "curve"; DatastreamRoom2["METADATA"] = "metadata"; DatastreamRoom2["HOLDERS"] = "holders"; DatastreamRoom2["TOKEN_CHANGES"] = "token"; DatastreamRoom2["POOL_CHANGES"] = "pool"; return DatastreamRoom2; })(DatastreamRoom || {}); var SubscriptionMethods = class { constructor(datastream) { this.ds = datastream; this.price = new PriceSubscriptions(datastream); this.tx = new TransactionSubscriptions(datastream); } /** * Subscribe to latest tokens and pools */ latest() { return this.ds._subscribe("latest"); } /** * Subscribe to graduating tokens * @param marketCapThresholdSOL Optional market cap threshold in SOL */ graduating(marketCapThresholdSOL) { const room = marketCapThresholdSOL ? `graduating:sol:${marketCapThresholdSOL}` : "graduating"; return this.ds._subscribe(room); } /** * Subscribe to tokens reaching a specific curve percentage for a market * @param market The market type: 'launchpad', 'pumpfun', 'boop', or 'meteora-curve' * @param percentage The curve percentage threshold (e.g., 30, 50, 75) * @returns Subscription response with curve percentage updates */ curvePercentage(market, percentage) { if (percentage < 0 || percentage > 100) { throw new Error("Percentage must be between 30 and 100"); } const room = `${market}:curve:${percentage}`; return this.ds._subscribe(room); } /** * Subscribe to graduated tokens */ graduated() { return this.ds._subscribe("graduated"); } /** * Subscribe to token metadata updates * @param tokenAddress The token address */ metadata(tokenAddress) { return this.ds._subscribe(`metadata:${tokenAddress}`); } /** * Subscribe to holder count updates for a token * @param tokenAddress The token address */ holders(tokenAddress) { return this.ds._subscribe(`holders:${tokenAddress}`); } /** * Subscribe to token changes (any pool) * @param tokenAddress The token address */ token(tokenAddress) { return this.ds._subscribe(`token:${tokenAddress}`); } /** * Subscribe to pool changes * @param poolId The pool address */ pool(poolId) { return this.ds._subscribe(`pool:${poolId}`); } }; var PriceSubscriptions = class { constructor(datastream) { this.ds = datastream; } /** * Subscribe to price updates for a token's primary/largest pool * @param tokenAddress The token address */ token(tokenAddress) { return this.ds._subscribe(`price-by-token:${tokenAddress}`); } /** * Subscribe to all price updates for a token across all pools * @param tokenAddress The token address */ allPoolsForToken(tokenAddress) { return this.ds._subscribe(`price:${tokenAddress}`); } /** * Subscribe to price updates for a specific pool * @param poolId The pool address */ pool(poolId) { return this.ds._subscribe(`price:${poolId}`); } }; var TransactionSubscriptions = class { constructor(datastream) { this.ds = datastream; } /** * Subscribe to transactions for a token across all pools * @param tokenAddress The token address */ token(tokenAddress) { return this.ds._subscribe(`transaction:${tokenAddress}`); } /** * Subscribe to transactions for a specific token and pool * @param tokenAddress The token address * @param poolId The pool address */ pool(tokenAddress, poolId) { return this.ds._subscribe(`transaction:${tokenAddress}:${poolId}`); } /** * Subscribe to transactions for a specific wallet * @param walletAddress The wallet address */ wallet(walletAddress) { return this.ds._subscribe(`wallet:${walletAddress}`); } }; var Datastream = class extends EventEmitter { /** * Creates a new Datastream client for real-time Solana Tracker data * @param config Configuration options */ constructor(config) { super(); this.socket = null; this.transactionSocket = null; this.reconnectAttempts = 0; this.subscribedRooms = /* @__PURE__ */ new Set(); this.transactions = /* @__PURE__ */ new Set(); this.isConnecting = false; this.worker = null; this.wsUrl = config.wsUrl || ""; this.autoReconnect = config.autoReconnect !== false; this.reconnectDelay = config.reconnectDelay || 2500; this.reconnectDelayMax = config.reconnectDelayMax || 4500; this.randomizationFactor = config.randomizationFactor || 0.5; this.useWorker = config.useWorker || false; this.workerUrl = config.workerUrl; this.subscribe = new SubscriptionMethods(this); if (typeof window !== "undefined") { window.addEventListener("beforeunload", this.disconnect.bind(this)); } } /** * Connects to the WebSocket server * @returns Promise that resolves when connected */ async connect() { if (this.useWorker) { return this.connectWithWorker(); } if (this.socket && this.transactionSocket) { return; } if (this.isConnecting) { return; } this.isConnecting = true; try { await Promise.all([ this.createSocket("main"), this.createSocket("transaction") ]); this.isConnecting = false; this.emit("connected"); } catch (e) { this.isConnecting = false; this.emit("error", e); if (this.autoReconnect) { this.reconnect(); } } } /** * Connects using Web Worker * @returns Promise that resolves when connected */ async connectWithWorker() { if (this.worker) { return; } if (this.isConnecting) { return; } this.isConnecting = true; try { if (this.workerUrl) { this.worker = new Worker(this.workerUrl); } else { const workerCode = this.getWorkerCode(); const blob = new Blob([workerCode], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(blob); this.worker = new Worker(workerUrl); } this.setupWorkerListeners(); this.worker.postMessage({ type: "connect", data: { wsUrl: this.wsUrl, autoReconnect: this.autoReconnect, reconnectDelay: this.reconnectDelay, reconnectDelayMax: this.reconnectDelayMax, randomizationFactor: this.randomizationFactor } }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Worker connection timeout")); }, 1e4); const handler = (e) => { var _a, _b; if (e.data.type === "connected") { clearTimeout(timeout); (_a = this.worker) == null ? void 0 : _a.removeEventListener("message", handler); resolve(); } else if (e.data.type === "error") { clearTimeout(timeout); (_b = this.worker) == null ? void 0 : _b.removeEventListener("message", handler); reject(new Error(e.data.data)); } }; this.worker.addEventListener("message", handler); }); this.isConnecting = false; this.emit("connected"); } catch (e) { this.isConnecting = false; this.emit("error", e); if (this.autoReconnect) { this.reconnect(); } } } /** * Sets up worker event listeners */ setupWorkerListeners() { if (!this.worker) return; this.worker.onmessage = (event) => { const { type, data, socketType } = event.data; switch (type) { case "message": this.handleWorkerMessage(data); break; case "disconnected": this.emit("disconnected", socketType || "all"); if (socketType === "all") { this.worker = null; } break; case "error": this.emit("error", new Error(data)); break; case "reconnecting": this.emit("reconnecting", data); break; } }; this.worker.onerror = (error) => { this.emit("error", error); }; } /** * Handles messages from worker */ handleWorkerMessage(data) { const { room, message } = data; if ((message == null ? void 0 : message.tx) && this.transactions.has(message.tx)) { return; } else if (message == null ? void 0 : message.tx) { this.transactions.add(message.tx); } if (room.includes("price:")) { this.emit(`price-by-token:${message.token}`, message); } this.emit(room, message); } /** * Gets the worker code as a string */ getWorkerCode() { return ` let mainSocket = null; let transactionSocket = null; let config = {}; let reconnectAttempts = 0; let subscribedRooms = new Set(); let transactions = new Set(); self.addEventListener('message', (event) => { const { type, data } = event.data; switch (type) { case 'connect': config = data; connect(); break; case 'disconnect': disconnect(); break; case 'subscribe': subscribe(data.room); break; case 'unsubscribe': unsubscribe(data.room); break; case 'send': send(data.socket, data.message); break; } }); async function connect() { try { await Promise.all([ createSocket('main'), createSocket('transaction') ]); self.postMessage({ type: 'connected' }); } catch (e) { self.postMessage({ type: 'error', data: e.message }); if (config.autoReconnect) { reconnect(); } } } function createSocket(type) { return new Promise((resolve, reject) => { try { const socket = new WebSocket(config.wsUrl); socket.onopen = () => { if (type === 'main') { mainSocket = socket; } else { transactionSocket = socket; } reconnectAttempts = 0; setupSocketListeners(socket, type); resubscribeToRooms(); resolve(); }; socket.onerror = (error) => { reject(error); }; } catch (e) { reject(e); } }); } function setupSocketListeners(socket, type) { socket.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'message') { self.postMessage({ type: 'message', data: { room: message.room, message: message.data } }); } } catch (error) { self.postMessage({ type: 'error', data: error.message }); } }; socket.onclose = () => { if (type === 'main') { mainSocket = null; } else if (type === 'transaction') { transactionSocket = null; } self.postMessage({ type: 'disconnected', socketType: type }); if (config.autoReconnect) { reconnect(); } }; } function disconnect() { if (mainSocket) { mainSocket.close(); mainSocket = null; } if (transactionSocket) { transactionSocket.close(); transactionSocket = null; } subscribedRooms.clear(); transactions.clear(); self.postMessage({ type: 'disconnected', socketType: 'all' }); } function reconnect() { self.postMessage({ type: 'reconnecting', data: reconnectAttempts }); const delay = Math.min( config.reconnectDelay * Math.pow(2, reconnectAttempts), config.reconnectDelayMax ); const jitter = delay * config.randomizationFactor; const reconnectDelay = delay + Math.random() * jitter; setTimeout(() => { reconnectAttempts++; connect(); }, reconnectDelay); } function subscribe(room) { subscribedRooms.add(room); const socket = room.includes('transaction') ? transactionSocket : mainSocket; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'join', room })); } } function unsubscribe(room) { subscribedRooms.delete(room); const socket = room.includes('transaction') ? transactionSocket : mainSocket; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'leave', room })); } } function send(socketType, message) { const socket = socketType === 'transaction' ? transactionSocket : mainSocket; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(message); } } function resubscribeToRooms() { if (mainSocket && mainSocket.readyState === WebSocket.OPEN && transactionSocket && transactionSocket.readyState === WebSocket.OPEN) { for (const room of subscribedRooms) { const socket = room.includes('transaction') ? transactionSocket : mainSocket; socke