delegate-framework
Version:
A TypeScript framework for building robust, production-ready blockchain workflows with comprehensive error handling, logging, and testing. Maintained by delegate.fun
914 lines • 84.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HeliusClient = void 0;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const bs58_1 = __importDefault(require("bs58"));
const error_handling_1 = require("../../utils/error-handling");
class HeliusClient {
constructor(config) {
this.requestId = 0;
// Rate limit tracking
this.rateLimitInfo = {
remaining: -1,
limit: -1,
reset: -1,
lastUpdate: 0
};
this.config = {
rpcUrl: HeliusClient.DEFAULT_RPC_URL,
enhancedApiUrl: HeliusClient.DEFAULT_ENHANCED_API_URL,
timeout: HeliusClient.DEFAULT_TIMEOUT,
retries: HeliusClient.DEFAULT_RETRIES,
...config,
};
this.logger = this.config.logger;
}
/**
* Send a transaction to the Solana network
* @param transaction - The transaction to send
* @param options - Optional configuration for the transaction
* @returns Transaction signature
*/
async sendTransaction(transaction, options = {}) {
return this.makeRequest('sendTransaction', [
bs58_1.default.encode(transaction.serialize()),
{
encoding: options.encoding || 'base58',
skipPreflight: options.skipPreflight ?? false,
preflightCommitment: options.preflightCommitment || 'confirmed',
}
]);
}
/**
* Send native SOL transfer
* @param from - Source wallet keypair
* @param to - Destination wallet public key
* @param amount - Amount in lamports
* @param options - Optional transaction options
* @returns Transaction signature
*/
async sendNativeTransfer(from, to, amount, options = {}) {
const transaction = new web3_js_1.Transaction();
try {
// Get recent blockhash with retry mechanism
this.logger?.debug('Fetching recent blockhash for native transfer...');
const blockhashResponse = await this.getLatestBlockhashWithRetry();
// Extract blockhash from the correct response structure
let blockhash;
if (blockhashResponse?.value?.blockhash) {
// JSON-RPC response structure: { value: { blockhash: "...", lastValidBlockHeight: ... } }
blockhash = blockhashResponse.value.blockhash;
}
else if (blockhashResponse?.blockhash) {
// Direct blockhash property (backward compatibility)
blockhash = blockhashResponse.blockhash;
}
else {
throw new Error(`Invalid blockhash response structure: ${JSON.stringify(blockhashResponse)}`);
}
// Comprehensive validation
if (!blockhash) {
throw new Error('Blockhash is null or undefined');
}
if (typeof blockhash !== 'string') {
throw new Error(`Blockhash is not a string: ${typeof blockhash}`);
}
if (blockhash.length === 0) {
throw new Error('Blockhash is empty string');
}
// Validate blockhash format (base58, typical length)
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(blockhash)) {
throw new Error(`Invalid blockhash format: ${blockhash}`);
}
this.logger?.debug(`Blockhash obtained: ${blockhash}`);
// CORRECT ORDER: Set fee payer first, then add instructions, then set blockhash
transaction.feePayer = from.publicKey;
transaction.add(web3_js_1.SystemProgram.transfer({
fromPubkey: from.publicKey,
toPubkey: to,
lamports: amount,
}));
// Set the blockhash AFTER adding instructions
transaction.recentBlockhash = blockhash;
// Verify transaction state before signing
this.logger?.debug('Transaction state before signing:', {
hasBlockhash: !!transaction.recentBlockhash,
blockhash: transaction.recentBlockhash,
feePayer: transaction.feePayer?.toString(),
instructions: transaction.instructions.length
});
// Verify blockhash is set before signing
if (!transaction.recentBlockhash) {
throw new Error('Transaction blockhash not set before signing');
}
// Sign the transaction
transaction.sign(from);
return this.sendTransaction(transaction, options);
}
catch (error) {
this.logger?.error('Error in sendNativeTransfer:', error);
throw error;
}
}
/**
* Send SPL token transfer
* @param to - Destination token account public key
* @param owner - Owner keypair of the source token account
* @param amount - Amount to transfer
* @param mint - Token mint address
* @param options - Optional transaction options
* @returns Transaction signature
*/
async sendTokenTransfer(to, owner, amount, mint, options = {}) {
const transaction = new web3_js_1.Transaction();
try {
// Get the source token account for this owner and mint
const sourceTokenAccount = await this.getTokenAccount(owner.publicKey, mint);
if (!sourceTokenAccount || !sourceTokenAccount.value || sourceTokenAccount.value.length === 0) {
throw new Error(`No token account found for owner ${owner.publicKey.toString()} and mint ${mint.toString()}`);
}
const fromTokenAccount = new web3_js_1.PublicKey(sourceTokenAccount.value[0].pubkey);
// Get recent blockhash with retry mechanism
this.logger?.debug('Fetching recent blockhash for token transfer...');
const blockhashResponse = await this.getLatestBlockhashWithRetry();
// Extract blockhash from the correct response structure
let blockhash;
if (blockhashResponse?.value?.blockhash) {
// JSON-RPC response structure: { value: { blockhash: "...", lastValidBlockHeight: ... } }
blockhash = blockhashResponse.value.blockhash;
}
else if (blockhashResponse?.blockhash) {
// Direct blockhash property (backward compatibility)
blockhash = blockhashResponse.blockhash;
}
else {
throw new Error(`Invalid blockhash response structure: ${JSON.stringify(blockhashResponse)}`);
}
// Comprehensive validation
if (!blockhash) {
throw new Error('Blockhash is null or undefined');
}
if (typeof blockhash !== 'string') {
throw new Error(`Blockhash is not a string: ${typeof blockhash}`);
}
if (blockhash.length === 0) {
throw new Error('Blockhash is empty string');
}
// Validate blockhash format (base58, typical length)
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(blockhash)) {
throw new Error(`Invalid blockhash format: ${blockhash}`);
}
this.logger?.debug(`Blockhash obtained: ${blockhash}`);
// Set fee payer first
transaction.feePayer = owner.publicKey;
// Add token transfer instruction
transaction.add((0, spl_token_1.createTransferInstruction)(fromTokenAccount, to, owner.publicKey, amount, [], spl_token_1.TOKEN_PROGRAM_ID));
// Set the blockhash AFTER adding instructions
transaction.recentBlockhash = blockhash;
// Verify transaction state before signing
this.logger?.debug('Transaction state before signing:', {
hasBlockhash: !!transaction.recentBlockhash,
blockhash: transaction.recentBlockhash,
feePayer: transaction.feePayer?.toString(),
instructions: transaction.instructions.length
});
// Verify blockhash is set before signing
if (!transaction.recentBlockhash) {
throw new Error('Transaction blockhash not set before signing');
}
// Sign the transaction
transaction.sign(owner);
return this.sendTransaction(transaction, options);
}
catch (error) {
this.logger?.error('Error in sendTokenTransfer:', error);
throw error;
}
}
/**
* Get the balance of a public key
* @param publicKey - The public key to check balance for
* @returns Balance in lamports
*/
async getBalance(publicKey) {
return this.makeRequest('getBalance', [publicKey.toString()]);
}
/**
* Get account information with enhanced Metaplex metadata parsing
* @param publicKey - The public key to get account info for
* @param encodingOrOptions - Optional encoding or configuration options
* @returns Account information with optional parsed Metaplex metadata
*/
async getAccountInfo(publicKey, encodingOrOptions) {
let options;
if (typeof encodingOrOptions === 'string') {
options = { encoding: encodingOrOptions };
}
else {
options = encodingOrOptions || { encoding: 'base64' };
}
const { encoding = 'base64', parseMetaplexMetadata = false, includeOffChainMetadata = false } = options;
// Get basic account info
const accountInfo = await this.makeRequest('getAccountInfo', [
publicKey.toString(),
{ encoding }
]);
// If Metaplex metadata parsing is requested and this is a metadata account
if (parseMetaplexMetadata && accountInfo?.value?.owner === HeliusClient.METAPLEX_METADATA_PROGRAM_ID) {
return this.parseMetaplexMetadata(accountInfo, includeOffChainMetadata);
}
// If parseMetaplexMetadata is true but this isn't a metadata account, try to derive metadata from mint
if (parseMetaplexMetadata && accountInfo?.value?.owner !== HeliusClient.METAPLEX_METADATA_PROGRAM_ID) {
try {
const metadataAccount = await this.getMetaplexMetadataAccount(publicKey);
if (metadataAccount) {
return this.parseMetaplexMetadata(metadataAccount, includeOffChainMetadata);
}
}
catch (error) {
this.logger?.debug('Failed to derive metadata account from mint:', error);
}
}
return accountInfo;
}
/**
* Get transaction details
* @param signature - Transaction signature
* @param commitment - Optional commitment level
* @returns Transaction details
*/
async getTransaction(signature, commitment = 'confirmed') {
return this.makeRequest('getTransaction', [
signature,
{ commitment }
]);
}
/**
* Get transactions for a public key
* @param publicKey - The public key to get transactions for
* @param options - Optional configuration for transaction retrieval
* @returns Array of Transaction objects
*/
async getTransactions(publicKey, options = {}) {
// Debug logging at start of method
if (options.debug) {
this.debugLog(`Starting getTransactions request:`, {
publicKey: publicKey.toString(),
options: {
limit: options.limit || 'default',
before: options.before || 'none',
until: options.until || 'none',
debug: options.debug
}
});
}
// Log rate limit status before making request
const rateLimitInfo = this.getRateLimitInfo();
if (rateLimitInfo.remaining >= 0 && rateLimitInfo.remaining < 10) {
this.logger?.warn(`Low rate limit remaining before getTransactions: ${rateLimitInfo.remaining}/${rateLimitInfo.limit} (${rateLimitInfo.usagePercentage.toFixed(1)}% used)`);
}
// Build URL with query parameters
const baseUrl = this.config.enhancedApiUrl.replace(/\/$/, ''); // Remove trailing slash if present
const url = new URL(`${baseUrl}/addresses/${publicKey.toString()}/transactions`);
url.searchParams.set('api-key', this.config.apiKey);
// Add optional parameters if provided
if (options.limit !== undefined) {
if (options.limit <= 0) {
throw new Error('Limit must be greater than 0');
}
url.searchParams.set('limit', options.limit.toString());
}
if (options.before !== undefined) {
if (!options.before || typeof options.before !== 'string') {
throw new Error('Before parameter must be a non-empty string');
}
url.searchParams.set('before', options.before);
}
if (options.until !== undefined) {
if (!options.until || typeof options.until !== 'string') {
throw new Error('Until parameter must be a non-empty string');
}
url.searchParams.set('until', options.until);
}
// Validate parameter combinations
this.validatePaginationParameters(options);
const response = await this.makeRestRequest(url.toString());
// Ensure the response is an array of Transaction objects
if (!Array.isArray(response)) {
throw new Error('Invalid response format: expected array of transactions');
}
// Validate that each item has the required Transaction properties
const transactions = response.map((item, index) => {
if (!item || typeof item !== 'object') {
throw new Error(`Invalid transaction at index ${index}: not an object`);
}
if (typeof item.signature !== 'string') {
throw new Error(`Invalid transaction at index ${index}: missing or invalid signature`);
}
if (typeof item.slot !== 'number') {
throw new Error(`Invalid transaction at index ${index}: missing or invalid slot`);
}
if (typeof item.timestamp !== 'number') {
throw new Error(`Invalid transaction at index ${index}: missing or invalid timestamp`);
}
if (typeof item.description !== 'string') {
throw new Error(`Invalid transaction at index ${index}: missing or invalid description`);
}
// Ensure nativeTransfers and tokenTransfers are arrays
if (!Array.isArray(item.nativeTransfers)) {
throw new Error(`Invalid transaction at index ${index}: nativeTransfers must be an array`);
}
if (!Array.isArray(item.tokenTransfers)) {
throw new Error(`Invalid transaction at index ${index}: tokenTransfers must be an array`);
}
return item;
});
// Debug logging for batch signatures
if (options.debug) {
if (transactions.length > 0) {
const firstTransaction = transactions[0];
const lastTransaction = transactions[transactions.length - 1];
if (firstTransaction && lastTransaction) {
const firstSignature = firstTransaction.signature;
const lastSignature = lastTransaction.signature;
const firstSlot = firstTransaction.slot;
const lastSlot = lastTransaction.slot;
const firstTimestamp = new Date(firstTransaction.timestamp * 1000).toISOString();
const lastTimestamp = new Date(lastTransaction.timestamp * 1000).toISOString();
this.debugLog(`Batch debug info:`, {
batchSize: transactions.length,
firstSignature: `${firstSignature} (slot: ${firstSlot}, time: ${firstTimestamp})`,
lastSignature: `${lastSignature} (slot: ${lastSlot}, time: ${lastTimestamp})`,
paginationParams: {
before: options.before || 'none',
until: options.until || 'none',
limit: options.limit || 'default'
},
publicKey: publicKey.toString()
});
}
}
else {
this.debugLog(`No transactions found:`, {
publicKey: publicKey.toString(),
paginationParams: {
before: options.before || 'none',
until: options.until || 'none',
limit: options.limit || 'default'
}
});
}
}
return transactions;
}
/**
* Validate pagination parameter combinations
* @param options - The pagination options to validate
* @private
*/
validatePaginationParameters(options) {
// Check for conflicting backward pagination parameters
if (options.before && options.until) {
this.logger?.warn('Both before and until parameters are provided. This may result in unexpected behavior.');
}
}
/**
* Get all transactions for a public key with automatic pagination
* @param publicKey - The public key to get all transactions for
* @param options - Optional configuration for transaction retrieval (supports all pagination parameters)
* @returns Array of all Transaction objects
*/
async getAllTransactions(publicKey, options = {}) {
// Log initial rate limit status
const initialRateLimit = this.getRateLimitInfo();
this.logger?.info(`Starting getAllTransactions: fetching all transactions in batches of ${options.limit || 100}`, {
publicKey: publicKey.toString(),
batchLimit: options.limit || 100,
initialRateLimit,
debugMode: options.debug || false
});
const allTransactions = [];
const batchLimit = options.limit || 100; // Default batch size
let batchCount = 0;
let consecutiveEmptyBatches = 0;
const maxConsecutiveEmptyBatches = 3; // Stop after 3 consecutive empty batches
// For backward pagination, we track the oldest signature for next batch
let paginationSignature = null;
let isFirstBatch = true;
while (true) {
batchCount++;
const batchOptions = {
limit: batchLimit
};
// Handle backward pagination - CRITICAL FIX: Don't use user's before/until for first batch
// if we want to get ALL transactions from the beginning
if (paginationSignature) {
batchOptions.before = paginationSignature;
}
else if (options.before || options.until) {
// If user provided before/until, use it for the first batch only
if (options.before)
batchOptions.before = options.before;
if (options.until)
batchOptions.until = options.until;
}
// If no pagination parameters, start from the most recent (default behavior)
// Debug logging for getAllTransactions batch start
if (options.debug) {
this.debugLog(`Starting getAllTransactions batch ${batchCount}:`, {
batchNumber: batchCount,
requestedLimit: batchLimit,
totalRetrieved: allTransactions.length,
paginationSignature: paginationSignature || 'none',
isFirstBatch,
userBefore: options.before || 'none',
userUntil: options.until || 'none'
});
}
// Check rate limit before each batch
const batchRateLimit = this.getRateLimitInfo();
if (batchRateLimit.remaining >= 0 && batchRateLimit.remaining < 5) {
this.logger?.warn(`Very low rate limit before getAllTransactions batch ${batchCount}: ${batchRateLimit.remaining}/${batchRateLimit.limit} remaining`);
}
try {
const transactions = await this.getTransactions(publicKey, batchOptions);
if (transactions && transactions.length > 0) {
// Reset consecutive empty batches counter
consecutiveEmptyBatches = 0;
this.logger?.debug(`Fetched batch ${batchCount} of ${transactions.length} transactions`);
// For subsequent batches, we need to handle potential overlap
// The 'before' parameter is exclusive, so we might have gaps
if (!isFirstBatch && allTransactions.length > 0) {
const lastPreviousTransaction = allTransactions[allTransactions.length - 1];
const firstCurrentTransaction = transactions[0];
if (lastPreviousTransaction && firstCurrentTransaction) {
const lastPreviousSignature = lastPreviousTransaction.signature;
const firstCurrentSignature = firstCurrentTransaction.signature;
// Check if there's a gap between batches
if (lastPreviousSignature !== firstCurrentSignature) {
this.logger?.warn(`Gap detected between batches ${batchCount - 1} and ${batchCount}. Last previous: ${lastPreviousSignature}, First current: ${firstCurrentSignature}`);
// Try to fill the gap by requesting transactions between these signatures
try {
const gapOptions = {
limit: Math.min(100, batchLimit * 2), // Use larger limit for gap filling
before: lastPreviousSignature,
until: firstCurrentSignature
};
this.logger?.debug(`Attempting to fill gap with until parameter: ${firstCurrentSignature}`);
const gapTransactions = await this.getTransactions(publicKey, gapOptions);
if (gapTransactions && gapTransactions.length > 0) {
this.logger?.info(`Successfully filled gap with ${gapTransactions.length} transactions`);
allTransactions.push(...gapTransactions);
}
}
catch (gapError) {
this.logger?.warn('Failed to fill gap:', gapError);
}
}
}
}
allTransactions.push(...transactions);
isFirstBatch = false;
// Debug logging for getAllTransactions batch completion
if (options.debug && transactions.length > 0) {
const firstTransaction = transactions[0];
const lastTransaction = transactions[transactions.length - 1];
if (firstTransaction && lastTransaction) {
this.debugLog(`getAllTransactions batch ${batchCount} completed:`, {
batchNumber: batchCount,
batchSize: transactions.length,
firstSignature: firstTransaction.signature,
lastSignature: lastTransaction.signature,
firstSlot: firstTransaction.slot,
lastSlot: lastTransaction.slot,
firstTimestamp: new Date(firstTransaction.timestamp * 1000).toISOString(),
lastTimestamp: new Date(lastTransaction.timestamp * 1000).toISOString(),
totalRetrieved: allTransactions.length,
isFirstBatch: false
});
}
}
// Update pagination signature for backward pagination
// Use the oldest transaction signature for the next 'before' parameter
const lastTransaction = transactions[transactions.length - 1];
if (lastTransaction) {
paginationSignature = lastTransaction.signature;
this.logger?.debug(`Batch ${batchCount}: Next pagination signature: ${paginationSignature}`);
}
// If we got fewer transactions than requested, we've reached the end
if (transactions.length < batchLimit) {
this.logger?.debug(`Batch ${batchCount}: Reached end of transactions (got ${transactions.length} < ${batchLimit})`);
break;
}
}
else {
consecutiveEmptyBatches++;
this.logger?.warn(`Batch ${batchCount}: No transactions found. Consecutive empty batches: ${consecutiveEmptyBatches}`);
// If we've had multiple consecutive empty batches, we might be at the end
if (consecutiveEmptyBatches >= maxConsecutiveEmptyBatches) {
this.logger?.warn(`Stopping after ${maxConsecutiveEmptyBatches} consecutive empty batches`);
break;
}
// Wait before retrying
await this.delay(Math.pow(2, consecutiveEmptyBatches) * 1000);
}
}
catch (error) {
this.logger?.error(`Batch ${batchCount} failed:`, error);
consecutiveEmptyBatches++;
if (consecutiveEmptyBatches >= maxConsecutiveEmptyBatches) {
this.logger?.error(`Stopping after ${maxConsecutiveEmptyBatches} consecutive failures`);
break;
}
// Wait before retrying
await this.delay(Math.pow(2, consecutiveEmptyBatches) * 1000);
}
}
// Log final rate limit status
const finalRateLimit = this.getRateLimitInfo();
this.logger?.info(`Finished fetching all transactions. Total: ${allTransactions.length} in ${batchCount} batches`, {
finalRateLimit,
totalTransactions: allTransactions.length,
batchesProcessed: batchCount,
consecutiveEmptyBatches
});
// Log a warning if we didn't get many transactions
if (allTransactions.length < batchLimit * 2) {
this.logger?.warn(`Only retrieved ${allTransactions.length} transactions. This might indicate limited transaction history or pagination issues.`);
}
return allTransactions;
}
/**
* Get a specific number of transactions for a public key with automatic pagination
* @param publicKey - The public key to get transactions for
* @param totalLimit - Total number of transactions to fetch
* @param options - Optional configuration for transaction retrieval (supports all pagination parameters)
* @param batchSize - Number of transactions to fetch per API call (default: 10, max: 100)
* @returns Array of Transaction objects up to the specified limit
*/
async getTransactionsWithLimit(publicKey, totalLimit, options = {}, batchSize = 10) {
if (batchSize <= 0 || batchSize > 100) {
throw new Error('Batch size must be between 1 and 100');
}
// Log initial rate limit status
const initialRateLimit = this.getRateLimitInfo();
this.logger?.info(`Starting getTransactionsWithLimit: requesting ${totalLimit} transactions in batches of ${batchSize}`, {
publicKey: publicKey.toString(),
totalLimit,
batchSize,
initialRateLimit,
debugMode: options.debug || false
});
const transactions = [];
let batchCount = 0;
let consecutiveEmptyBatches = 0;
const maxConsecutiveEmptyBatches = 3; // Stop after 3 consecutive empty batches
// For backward pagination, we track the oldest signature for next batch
let paginationSignature = null;
let isFirstBatch = true;
let lastBatchSize = 0;
while (transactions.length < totalLimit) {
batchCount++;
const remainingLimit = totalLimit - transactions.length;
const currentBatchLimit = Math.min(batchSize, remainingLimit);
const batchOptions = {
...options,
limit: currentBatchLimit
};
// Handle backward pagination
if (paginationSignature) {
batchOptions.before = paginationSignature;
// Remove the original until parameter for subsequent batches
delete batchOptions.until;
}
// For the first batch, keep the original 'before' or 'until' parameter
// Debug logging for batch start
if (options.debug) {
this.debugLog(`Starting batch ${batchCount}:`, {
batchNumber: batchCount,
requestedLimit: currentBatchLimit,
totalRetrieved: transactions.length,
remainingToFetch: totalLimit - transactions.length,
paginationSignature: paginationSignature || 'none'
});
}
this.logger?.debug(`Batch ${batchCount}: Requesting ${currentBatchLimit} transactions${paginationSignature ? ` before ${paginationSignature}` : ''}`);
// Check rate limit before each batch
const batchRateLimit = this.getRateLimitInfo();
if (batchRateLimit.remaining >= 0 && batchRateLimit.remaining < 5) {
this.logger?.warn(`Very low rate limit before batch ${batchCount}: ${batchRateLimit.remaining}/${batchRateLimit.limit} remaining`);
}
const batchTransactions = await this.getTransactions(publicKey, batchOptions);
if (batchTransactions && batchTransactions.length > 0) {
// Reset consecutive empty batches counter
consecutiveEmptyBatches = 0;
// For subsequent batches, we need to handle potential overlap
// The 'before' parameter is exclusive, so we might have gaps
if (!isFirstBatch && transactions.length > 0) {
const lastPreviousTransaction = transactions[transactions.length - 1];
const firstCurrentTransaction = batchTransactions[0];
if (lastPreviousTransaction && firstCurrentTransaction) {
const lastPreviousSignature = lastPreviousTransaction.signature;
const firstCurrentSignature = firstCurrentTransaction.signature;
// Check if there's a gap between batches
if (lastPreviousSignature !== firstCurrentSignature) {
this.logger?.warn(`Potential gap detected between batches. Last previous: ${lastPreviousSignature}, First current: ${firstCurrentSignature}`);
// If we detect a gap, we might need to adjust our pagination strategy
// For now, we'll continue but log the issue
}
}
}
transactions.push(...batchTransactions);
isFirstBatch = false;
lastBatchSize = batchTransactions.length;
this.logger?.debug(`Batch ${batchCount}: Received ${batchTransactions.length} transactions. Total so far: ${transactions.length}`);
// Debug logging for batch completion
if (options.debug && batchTransactions.length > 0) {
const firstTransaction = batchTransactions[0];
const lastTransaction = batchTransactions[batchTransactions.length - 1];
if (firstTransaction && lastTransaction) {
this.debugLog(`Batch ${batchCount} completed:`, {
batchNumber: batchCount,
batchSize: batchTransactions.length,
firstSignature: firstTransaction.signature,
lastSignature: lastTransaction.signature,
firstSlot: firstTransaction.slot,
lastSlot: lastTransaction.slot,
firstTimestamp: new Date(firstTransaction.timestamp * 1000).toISOString(),
lastTimestamp: new Date(lastTransaction.timestamp * 1000).toISOString(),
totalRetrieved: transactions.length,
progress: `${((transactions.length / totalLimit) * 100).toFixed(1)}%`
});
}
}
// Update pagination signature for backward pagination
// Use the oldest transaction signature for the next 'before' parameter
const lastTransaction = batchTransactions[batchTransactions.length - 1];
if (lastTransaction) {
paginationSignature = lastTransaction.signature;
this.logger?.debug(`Batch ${batchCount}: Next pagination signature: ${paginationSignature}`);
}
// If we got fewer transactions than requested, we've reached the end
if (batchTransactions.length < currentBatchLimit) {
this.logger?.debug(`Batch ${batchCount}: Reached end of transactions (got ${batchTransactions.length} < ${currentBatchLimit})`);
break;
}
}
else {
consecutiveEmptyBatches++;
this.logger?.warn(`Batch ${batchCount}: No transactions found. Consecutive empty batches: ${consecutiveEmptyBatches}`);
// If we've had multiple consecutive empty batches, we might be at the end
// or there might be an issue with our pagination
if (consecutiveEmptyBatches >= maxConsecutiveEmptyBatches) {
this.logger?.warn(`Stopping after ${maxConsecutiveEmptyBatches} consecutive empty batches`);
break;
}
// If this is not the first batch and we got an empty response,
// we might need to try a different pagination strategy
if (!isFirstBatch && lastBatchSize > 0) {
this.logger?.warn(`Empty batch after receiving ${lastBatchSize} transactions in previous batch. This might indicate a pagination issue.`);
}
break;
}
}
// Log final rate limit status
const finalRateLimit = this.getRateLimitInfo();
this.logger?.info(`Finished fetching transactions with limit. Total: ${transactions.length}/${totalLimit} in ${batchCount} batches`, {
finalRateLimit,
batchesProcessed: batchCount,
successRate: `${((transactions.length / totalLimit) * 100).toFixed(1)}%`
});
// Log a warning if we didn't reach the requested limit
if (transactions.length < totalLimit) {
this.logger?.warn(`Only retrieved ${transactions.length} transactions out of ${totalLimit} requested. This might indicate missing transactions due to pagination gaps.`);
}
return transactions;
}
/**
* Get transactions with limit using a more robust pagination strategy
* This method attempts to handle potential gaps in transaction history by using
* a combination of 'before' and 'until' parameters and retry logic
* @param publicKey - The public key to get transactions for
* @param totalLimit - Total number of transactions to fetch
* @param options - Optional configuration for transaction retrieval
* @param batchSize - Number of transactions to fetch per API call (default: 50, max: 100)
* @returns Array of Transaction objects up to the specified limit
*/
async getTransactionsWithLimitRobust(publicKey, totalLimit, options = {}, batchSize = 50) {
if (batchSize <= 0 || batchSize > 100) {
throw new Error('Batch size must be between 1 and 100');
}
// Log initial rate limit status
const initialRateLimit = this.getRateLimitInfo();
this.logger?.info(`Starting getTransactionsWithLimitRobust: requesting ${totalLimit} transactions in batches of ${batchSize}`, {
publicKey: publicKey.toString(),
totalLimit,
batchSize,
initialRateLimit,
debugMode: options.debug || false
});
const transactions = [];
let batchCount = 0;
let paginationSignature = null;
let retryCount = 0;
const maxRetries = 3;
while (transactions.length < totalLimit && retryCount < maxRetries) {
batchCount++;
const remainingLimit = totalLimit - transactions.length;
const currentBatchLimit = Math.min(batchSize, remainingLimit);
const batchOptions = {
...options,
limit: currentBatchLimit
};
// Handle backward pagination
if (paginationSignature) {
batchOptions.before = paginationSignature;
// Remove the original until parameter for subsequent batches
delete batchOptions.until;
}
// Debug logging for robust batch start
if (options.debug) {
this.debugLog(`Starting robust batch ${batchCount}:`, {
batchNumber: batchCount,
requestedLimit: currentBatchLimit,
totalRetrieved: transactions.length,
remainingToFetch: totalLimit - transactions.length,
paginationSignature: paginationSignature || 'none',
retryCount
});
}
this.logger?.debug(`Robust Batch ${batchCount}: Requesting ${currentBatchLimit} transactions${paginationSignature ? ` before ${paginationSignature}` : ''}`);
// Check rate limit before each batch
const batchRateLimit = this.getRateLimitInfo();
if (batchRateLimit.remaining >= 0 && batchRateLimit.remaining < 5) {
this.logger?.warn(`Very low rate limit before robust batch ${batchCount}: ${batchRateLimit.remaining}/${batchRateLimit.limit} remaining`);
}
try {
const batchTransactions = await this.getTransactions(publicKey, batchOptions);
if (batchTransactions && batchTransactions.length > 0) {
// Check for potential gaps
if (transactions.length > 0 && batchTransactions.length > 0) {
const lastPreviousTransaction = transactions[transactions.length - 1];
const firstCurrentTransaction = batchTransactions[0];
if (lastPreviousTransaction && firstCurrentTransaction) {
const lastPreviousSignature = lastPreviousTransaction.signature;
const firstCurrentSignature = firstCurrentTransaction.signature;
if (lastPreviousSignature !== firstCurrentSignature) {
this.logger?.warn(`Gap detected in robust method. Last previous: ${lastPreviousSignature}, First current: ${firstCurrentSignature}`);
// Try to fill the gap by requesting transactions between these signatures
try {
const gapOptions = {
...options,
limit: Math.min(100, totalLimit - transactions.length),
before: lastPreviousSignature,
until: firstCurrentSignature
};
this.logger?.debug(`Attempting to fill gap with until parameter: ${firstCurrentSignature}`);
const gapTransactions = await this.getTransactions(publicKey, gapOptions);
if (gapTransactions && gapTransactions.length > 0) {
this.logger?.info(`Successfully filled gap with ${gapTransactions.length} transactions`);
transactions.push(...gapTransactions);
}
}
catch (gapError) {
this.logger?.warn('Failed to fill gap:', gapError);
}
}
}
}
transactions.push(...batchTransactions);
retryCount = 0; // Reset retry count on successful batch
this.logger?.debug(`Robust Batch ${batchCount}: Received ${batchTransactions.length} transactions. Total so far: ${transactions.length}`);
// Debug logging for robust batch completion
if (options.debug && batchTransactions.length > 0) {
const firstTransaction = batchTransactions[0];
const lastTransaction = batchTransactions[batchTransactions.length - 1];
if (firstTransaction && lastTransaction) {
this.debugLog(`Robust batch ${batchCount} completed:`, {
batchNumber: batchCount,
batchSize: batchTransactions.length,
firstSignature: firstTransaction.signature,
lastSignature: lastTransaction.signature,
firstSlot: firstTransaction.slot,
lastSlot: lastTransaction.slot,
firstTimestamp: new Date(firstTransaction.timestamp * 1000).toISOString(),
lastTimestamp: new Date(lastTransaction.timestamp * 1000).toISOString(),
totalRetrieved: transactions.length,
progress: `${((transactions.length / totalLimit) * 100).toFixed(1)}%`,
retryCount
});
}
}
// Update pagination signature for backward pagination
const lastTransaction = batchTransactions[batchTransactions.length - 1];
if (lastTransaction) {
paginationSignature = lastTransaction.signature;
this.logger?.debug(`Robust Batch ${batchCount}: Next pagination signature: ${paginationSignature}`);
}
// If we got fewer transactions than requested, we've reached the end
if (batchTransactions.length < currentBatchLimit) {
this.logger?.debug(`Robust Batch ${batchCount}: Reached end of transactions (got ${batchTransactions.length} < ${currentBatchLimit})`);
break;
}
}
else {
retryCount++;
this.logger?.warn(`Robust Batch ${batchCount}: No transactions found. Retry ${retryCount}/${maxRetries}`);
if (retryCount >= maxRetries) {
this.logger?.warn(`Stopping after ${maxRetries} consecutive empty batches`);
break;
}
// Wait before retrying
await this.delay(Math.pow(2, retryCount) * 1000);
}
}
catch (error) {
retryCount++;
this.logger?.error(`Robust Batch ${batchCount} failed:`, error);
if (retryCount >= maxRetries) {
this.logger?.error(`Stopping after ${maxRetries} consecutive failures`);
break;
}
// Wait before retrying
await this.delay(Math.pow(2, retryCount) * 1000);
}
}
// Log final rate limit status
const finalRateLimit = this.getRateLimitInfo();
this.logger?.info(`Finished robust transaction fetching. Total: ${transactions.length}/${totalLimit} in ${batchCount} batches`, {
finalRateLimit,
batchesProcessed: batchCount,
retriesUsed: retryCount,
successRate: `${((transactions.length / totalLimit) * 100).toFixed(1)}%`
});
if (transactions.length < totalLimit) {
this.logger?.warn(`Only retrieved ${transactions.length} transactions out of ${totalLimit} requested using robust method.`);
}
return transactions;
}
/**
* Diagnostic method to analyze transaction pagination behavior
* This method helps identify gaps and understand the pagination patterns
* @param publicKey - The public key to analyze
* @param sampleSize - Number of transactions to analyze (default: 1000)
* @param batchSize - Batch size for analysis (default: 100)
* @returns Analysis results including gap detection and pagination statistics
*/
async analyzeTransactionPagination(publicKey, sampleSize = 1000, batchSize = 100) {
this.logger?.info(`Starting pagination analysis for ${publicKey.toString()}`);
const transactions = [];
let batchCount = 0;
let paginationSignature = null;
const gaps = [];
const paginationIssues = [];
const recommendations = [];
while (transactions.length < sampleSize) {
batchCount++;
const remainingLimit = sampleSize - transactions.length;
const currentBatchLimit = Math.min(batchSize, remainingLimit);
const batchOptions = {
limit: currentBatchLimit
};
if (paginationSignature) {
batchOptions.before = paginationSignature;
}
this.logger?.debug(`Analysis Batch ${batchCount}: Requesting ${currentBatchLimit} transactions`);
try {
const batchTransactions = await this.getTransactions(publicKey, batchOptions);
if (batchTransactions && batchTransactions.length > 0) {
// Check for gaps between batches
if (transactions.length > 0 && batchTransactions.length > 0) {
const lastPreviousTransaction = transactions[transactions.length - 1];
const firstCurrentTransaction = batchTransactions[0];
if (lastPreviousTransaction && firstCurrentTransaction) {
const lastPreviousSignature = lastPreviousTransaction.signature;
const firstCurrentSignature = firstCurrentTransaction.signature;
if (lastPreviousSignature !== firstCurrentSignature) {
gaps.push({
before: lastPreviousSignature,
after: firstCurrentSignature,
estimatedGapSize: 0 // Will be calculated if we can fetch gap transactions
});
paginationIssues.push(`Gap detected between batches ${batchCount - 1} and ${batchCount}`);
// Try to estimate gap size
try {
const gapOptions = {
limit: 100,
before: lastPreviousSignature,
until: firstCurrentSignature
};
const gapTransactions = await this.getTransactions(publicKey, gapOptions);
if (gapTransactions && gapTransactions.length > 0) {
const lastGap = gaps[gaps.length - 1];
if (lastGap) {
lastGap.estimatedGapSize = gapTransactions.length;
this.logger?.info(`Gap contains approximately ${gapTransactions.length} transactions`);
}
}
}
catch (gapError) {
this.logger?.warn('Could not estimate gap