@solana-tracker/data-api
Version:
Official Solana Tracker Data API client for accessing Solana Data
1,387 lines (1,381 loc) • 66.3 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Client: () => Client,
DataApiError: () => DataApiError,
Datastream: () => Datastream,
DatastreamRoom: () => DatastreamRoom,
RateLimitError: () => RateLimitError,
ValidationError: () => ValidationError,
decodeBinaryEvents: () => decodeBinaryEvents,
processEvents: () => processEvents,
processEventsAsync: () => processEventsAsync
});
module.exports = __toCommonJS(index_exports);
// 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, details) {
super(message);
this.status = status;
this.code = code;
this.name = "DataApiError";
this.details = details;
}
};
var RateLimitError = class extends DataApiError {
constructor(message, retryAfter, details) {
super(message, 429, "RATE_LIMIT_EXCEEDED", details);
this.retryAfter = retryAfter;
this.name = "RateLimitError";
}
};
var ValidationError = class extends DataApiError {
constructor(message, details) {
super(message, 400, "VALIDATION_ERROR", details);
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) {
let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
let errorDetails = null;
try {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
errorDetails = await response.json();
if (typeof errorDetails === "string") {
errorMessage = errorDetails;
} else if (errorDetails && typeof errorDetails === "object") {
if (typeof errorDetails.message === "string") {
errorMessage = errorDetails.message;
} else if (typeof errorDetails.error === "string") {
errorMessage = errorDetails.error;
} else if (typeof errorDetails.detail === "string") {
errorMessage = errorDetails.detail;
} else if (typeof errorDetails.msg === "string") {
errorMessage = errorDetails.msg;
} else if (errorDetails.error && typeof errorDetails.error === "object") {
if (typeof errorDetails.error.message === "string") {
errorMessage = errorDetails.error.message;
} else if (typeof errorDetails.error.detail === "string") {
errorMessage = errorDetails.error.detail;
} else {
errorMessage = `API error: ${JSON.stringify(errorDetails.error)}`;
}
} else {
errorMessage = `API error: ${JSON.stringify(errorDetails)}`;
}
}
}
} catch (parseError) {
console.error("Failed to parse error response:", parseError);
}
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`);
}
const error2 = new RateLimitError(errorMessage, retryAfter ? parseInt(retryAfter) : void 0);
if (errorDetails) {
error2.details = errorDetails;
}
throw error2;
}
const error = new DataApiError(errorMessage, response.status);
if (errorDetails) {
error.details = errorDetails;
}
throw error;
}
return response.json();
} catch (error) {
if (error instanceof DataApiError) {
throw error;
}
throw new DataApiError(`An unexpected error occurred: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* 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 and market cap for a token
* @param tokenAddress The token's mint address
* @returns All-time high price and market cap 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");
}
/**
* Get graduated tokens
* @returns List of graduated tokens
*/
async getGraduatingTokens() {
return this.request("/tokens/multi/graduating");
}
// ======== 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}×tamp=${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}`);
}
async getChartData(tokenAddressOrParams, type, timeFrom, timeTo, marketCap, removeOutliers, dynamicPools, timezone, fastCache) {
var _a, _b, _c, _d, _e, _f, _g, _h;
let tokenAddress;
if (typeof tokenAddressOrParams === "object") {
const params2 = tokenAddressOrParams;
tokenAddress = params2.tokenAddress;
type = (_a = params2.type) != null ? _a : type;
timeFrom = (_b = params2.timeFrom) != null ? _b : timeFrom;
timeTo = (_c = params2.timeTo) != null ? _c : timeTo;
marketCap = (_d = params2.marketCap) != null ? _d : marketCap;
removeOutliers = (_e = params2.removeOutliers) != null ? _e : removeOutliers;
dynamicPools = (_f = params2.dynamicPools) != null ? _f : dynamicPools;
timezone = (_g = params2.timezone) != null ? _g : timezone;
fastCache = (_h = params2.fastCache) != null ? _h : fastCache;
} else {
tokenAddress = tokenAddressOrParams;
}
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");
if (dynamicPools === false) params.append("dynamicPools", "false");
if (timezone) {
if (timezone === "current") {
const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
params.append("timezone", currentTimezone);
} else {
params.append("timezone", timezone);
}
}
if (fastCache) params.append("fastCache", "true");
const query = params.toString() ? `?${params.toString()}` : "";
return this.request(`/chart/${tokenAddress}${query}`);
}
async getPoolChartData(tokenAddressOrParams, poolAddress, type, timeFrom, timeTo, marketCap, removeOutliers, timezone, fastCache) {
var _a, _b, _c, _d, _e, _f, _g;
let tokenAddress;
let actualPoolAddress;
if (typeof tokenAddressOrParams === "object") {
const params2 = tokenAddressOrParams;
tokenAddress = params2.tokenAddress;
actualPoolAddress = params2.poolAddress;
type = (_a = params2.type) != null ? _a : type;
timeFrom = (_b = params2.timeFrom) != null ? _b : timeFrom;
timeTo = (_c = params2.timeTo) != null ? _c : timeTo;
marketCap = (_d = params2.marketCap) != null ? _d : marketCap;
removeOutliers = (_e = params2.removeOutliers) != null ? _e : removeOutliers;
timezone = (_f = params2.timezone) != null ? _f : timezone;
fastCache = (_g = params2.fastCache) != null ? _g : fastCache;
} else {
tokenAddress = tokenAddressOrParams;
actualPoolAddress = poolAddress;
}
this.validatePublicKey(tokenAddress, "tokenAddress");
this.validatePublicKey(actualPoolAddress, "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");
if (timezone) {
if (timezone === "current") {
const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
params.append("timezone", currentTimezone);
} else {
params.append("timezone", timezone);
}
}
if (fastCache) params.append("fastCache", "true");
const query = params.toString() ? `?${params.toString()}` : "";
return this.request(`/chart/${tokenAddress}/${actualPoolAddress}${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
* @param holdingCheck Additional check for current holding value in wallet
* @returns Token-specific PnL data
*/
async getTokenPnL(wallet, tokenAddress, holdingCheck) {
this.validatePublicKey(wallet, "wallet");
this.validatePublicKey(tokenAddress, "tokenAddress");
const params = new URLSearchParams();
if (holdingCheck) params.append("holdingCheck", "true");
const query = params.toString() ? `?${params.toString()}` : "";
return this.request(`/pnl/${wallet}/${tokenAddress}${query}`);
}
// ======== 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 current subscription information including credits, plan, and billing details
* @returns Subscription information
*/
async getSubscription() {
return this.request("/subscription");
}
/**
* 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
var import_events = require("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";
DatastreamRoom2["SNIPERS"] = "sniper";
DatastreamRoom2["INSIDERS"] = "insider";
return DatastreamRoom2;
})(DatastreamRoom || {});
var SubscriptionMethods = class {
constructor(datastream) {
this.ds = datastream;
this.price = new PriceSubscriptions(datastream);
this.tx = new TransactionSubscriptions(datastream);
this.stats = new StatsSubscriptions(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 0 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-related events (all pools, primary pool, dev events, or top holders)
*
* @example
* // For all pool updates:
* datastream.subscribe.token('address').all().on(callback)
* // Or using shorthand:
* datastream.subscribe.token('address').on(callback)
*
* // For primary pool updates only:
* datastream.subscribe.token('address').primary().on(callback)
*
* // For dev holding updates:
* datastream.subscribe.token('address').dev.holding().on(callback)
*
* // For top 10 holders updates:
* datastream.subscribe.token('address').top10().on(callback)
*
* @param tokenAddress The token address
*/
token(tokenAddress) {
const ds = this.ds;
const devSubscriptions = {
holding: () => {
return ds._subscribe(`dev_holding:${tokenAddress}`);
}
};
const baseSubscription = ds._subscribe(`token:${tokenAddress}`);
const enhancedSubscription = {
...baseSubscription,
// Keep the original on method for convenience
on: (callback) => {
return baseSubscription.on(callback);
},
// New methods
all: () => {
return ds._subscribe(`token:${tokenAddress}`);
},
primary: () => {
return ds._subscribe(`token:${tokenAddress}:primary`);
},
dev: devSubscriptions,
top10: () => {
return ds._subscribe(`top10:${tokenAddress}`);
},
fees: () => {
return ds._subscribe(`fees:${tokenAddress}`);
}
};
return enhancedSubscription;
}
/**
* Subscribe to pool changes
* @param poolId The pool address
*/
pool(poolId) {
return this.ds._subscribe(`pool:${poolId}`);
}
/**
* Subscribe to sniper updates for a token
* @param tokenAddress The token address
*/
snipers(tokenAddress) {
return this.ds._subscribe(`sniper:${tokenAddress}`);
}
/**
* Subscribe to insider updates for a token
* @param tokenAddress The token address
*/
insiders(tokenAddress) {
return this.ds._subscribe(`insider:${tokenAddress}`);
}
/**
* Subscribe to wallet balance updates
*
* @example
* // For all balance updates:
* datastream.subscribe.wallet('address').balance().on(callback)
*
* // For specific token balance:
* datastream.subscribe.wallet('address').tokenBalance('token').on(callback)
*
* @param walletAddress The wallet address
*/
wallet(walletAddress) {
const ds = this.ds;
return {
balance: () => {
return ds._subscribe(
`wallet:${walletAddress}:balance`
);
},
tokenBalance: (tokenAddress) => {
return ds._subscribe(
`wallet:${walletAddress}:${tokenAddress}:balance`
);
}
};
}
};
var StatsSubscriptions = class {
constructor(datastream) {
this.ds = datastream;
}
/**
* Subscribe to live stats updates for a token across all timeframes
* @param tokenAddress The token address
* @returns Subscription response with stats updates
*/
token(tokenAddress) {
return this.ds._subscribe(`stats:token:${tokenAddress}`);
}
/**
* Subscribe to live stats updates for a specific pool across all timeframes
* @param poolId The pool address
* @returns Subscription response with stats updates
*/
pool(poolId) {
return this.ds._subscribe(`stats: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 wallet transactions
*
* @example
* // Subscribe to wallet transactions (default):
* datastream.subscribe.tx.wallet('address').on(callback)
*
* // Subscribe to wallet transactions (explicit):
* datastream.subscribe.tx.wallet('address').transactions().on(callback)
*
* @param walletAddress The wallet address
*/
wallet(walletAddress) {
const ds = this.ds;
const baseSubscription = ds._subscribe(`wallet:${walletAddress}`);
return {
...baseSubscription,
// Default on method for transactions
on: (callback) => {
return baseSubscription.on(callback);
},
// Explicit transactions method (not deprecated)
transactions: () => {
return ds._subscribe(`wallet:${walletAddress}`);
},
// Deprecated balance methods
balance: () => {
console.warn(
"datastream.subscribe.tx.wallet().balance() is deprecated. Please use datastream.subscribe.wallet().balance() instead. This method will be removed in a future version."
);
return ds._subscribe(
`wallet:${walletAddress}:balance`
);
},
tokenBalance: (tokenAddress) => {
console.warn(
"datastream.subscribe.tx.wallet().tokenBalance() is deprecated. Please use datastream.subscribe.wallet().tokenBalance() instead. This method will be removed in a future version."
);
return ds._subscribe(
`wallet:${walletAddress}:${tokenAddress}:balance`
);
}
};
}
};
var Datastream = class extends import_events.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;
t