Advanced Patterns
Complex multi-method workflows and production-ready patterns for Solana applications
Advanced Patterns
Master complex workflows combining multiple API methods to build production-ready Solana applications. Each pattern includes complete working code, error handling, and optimization strategies.
Payment Processing Pipeline
Build a robust payment processing system that monitors transactions, verifies confirmations, and handles edge cases like reorgs.
Use Case
Process customer payments in a Solana-based e-commerce platform, ensuring transactions are confirmed before fulfilling orders.
Architecture
sequenceDiagram
participant Customer
participant App
participant WebSocket
participant RPC
participant Database
Customer->>App: Initiate payment
App->>Customer: Return payment address
Customer->>Solana: Send transaction
WebSocket->>App: Transaction notification
App->>RPC: getTransaction (verify)
RPC->>App: Transaction details
App->>RPC: getSignatureStatuses (confirmations)
RPC->>App: Confirmation status
alt Confirmed
App->>Database: Mark payment complete
App->>Customer: Order fulfilled
else Failed/Reorg
App->>Customer: Retry required
endComplete Implementation
import { Connection, PublicKey, Commitment } from '@solana/web3.js';
interface PaymentConfig {
recipientAddress: string;
expectedAmount: number;
timeout: number; // milliseconds
requiredConfirmations: number;
}
class PaymentProcessor {
private connection: Connection;
private wsConnection: Connection;
constructor(rpcUrl: string, wsUrl: string) {
this.connection = new Connection(rpcUrl, 'confirmed');
this.wsConnection = new Connection(wsUrl, {
commitment: 'confirmed',
wsEndpoint: wsUrl,
});
}
/**
* Monitor and process a payment
*/
async processPayment(config: PaymentConfig): Promise<PaymentResult> {
const recipient = new PublicKey(config.recipientAddress);
const startTime = Date.now();
return new Promise((resolve, reject) => {
let subscriptionId: number | null = null;
let timeoutId: NodeJS.Timeout;
// Set timeout
timeoutId = setTimeout(() => {
if (subscriptionId !== null) {
this.wsConnection.removeAccountChangeListener(subscriptionId);
}
reject(new Error('Payment timeout exceeded'));
}, config.timeout);
// Subscribe to account changes
subscriptionId = this.wsConnection.onAccountChange(
recipient,
async (accountInfo, context) => {
try {
console.log(`Account changed at slot ${context.slot}`);
// Get recent signatures
const signatures = await this.connection.getSignaturesForAddress(
recipient,
{ limit: 10 },
'confirmed'
);
// Process each signature
for (const sigInfo of signatures) {
if (sigInfo.err) continue;
// Verify transaction
const result = await this.verifyPaymentTransaction(
sigInfo.signature,
config
);
if (result.valid) {
// Check confirmations
const confirmed = await this.waitForConfirmations(
sigInfo.signature,
config.requiredConfirmations
);
if (confirmed) {
clearTimeout(timeoutId);
if (subscriptionId !== null) {
this.wsConnection.removeAccountChangeListener(subscriptionId);
}
resolve({
success: true,
signature: sigInfo.signature,
amount: result.amount,
timestamp: Date.now(),
confirmations: config.requiredConfirmations,
});
return;
}
}
}
} catch (error) {
console.error('Error processing account change:', error);
}
},
'confirmed'
);
console.log(`Monitoring payments to ${config.recipientAddress}`);
});
}
/**
* Verify a transaction matches payment criteria
*/
private async verifyPaymentTransaction(
signature: string,
config: PaymentConfig
): Promise<{ valid: boolean; amount?: number }> {
try {
const tx = await this.connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
commitment: 'confirmed',
});
if (!tx || tx.meta?.err) {
return { valid: false };
}
const recipient = new PublicKey(config.recipientAddress);
const recipientIndex = tx.transaction.message.staticAccountKeys.findIndex(
key => key.equals(recipient)
);
if (recipientIndex === -1) {
return { valid: false };
}
// Check balance change
const preBalance = tx.meta.preBalances[recipientIndex];
const postBalance = tx.meta.postBalances[recipientIndex];
const amountReceived = (postBalance - preBalance) / 1e9; // Convert to SOL
if (amountReceived >= config.expectedAmount) {
return { valid: true, amount: amountReceived };
}
return { valid: false };
} catch (error) {
console.error('Error verifying transaction:', error);
return { valid: false };
}
}
/**
* Wait for required confirmations
*/
private async waitForConfirmations(
signature: string,
requiredConfirmations: number
): Promise<boolean> {
const maxAttempts = 30;
const delayMs = 2000;
for (let i = 0; i < maxAttempts; i++) {
try {
const statuses = await this.connection.getSignatureStatuses([signature]);
const status = statuses.value[0];
if (!status) {
await this.delay(delayMs);
continue;
}
if (status.err) {
console.error('Transaction failed:', status.err);
return false;
}
const confirmations = status.confirmations || 0;
console.log(`Confirmations: ${confirmations}/${requiredConfirmations}`);
if (confirmations >= requiredConfirmations) {
return true;
}
// Check if finalized
if (status.confirmationStatus === 'finalized') {
return true;
}
await this.delay(delayMs);
} catch (error) {
console.error('Error checking confirmations:', error);
await this.delay(delayMs);
}
}
return false;
}
/**
* Handle reorg detection
*/
async detectReorg(signature: string): Promise<boolean> {
try {
const statuses = await this.connection.getSignatureStatuses([signature]);
const status = statuses.value[0];
// If transaction no longer exists, possible reorg
if (!status) {
console.warn(`Transaction ${signature} not found - possible reorg`);
return true;
}
return false;
} catch (error) {
console.error('Error detecting reorg:', error);
return false;
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface PaymentResult {
success: boolean;
signature: string;
amount: number;
timestamp: number;
confirmations: number;
}
// Usage example
async function example() {
const processor = new PaymentProcessor(
'https://rpc.solana.com',
'wss://rpc.solana.com'
);
try {
const result = await processor.processPayment({
recipientAddress: 'YourWalletAddressHere',
expectedAmount: 0.1, // 0.1 SOL
timeout: 300000, // 5 minutes
requiredConfirmations: 15,
});
console.log('Payment processed:', result);
} catch (error) {
console.error('Payment processing failed:', error);
}
}Error Handling
class PaymentError extends Error {
constructor(
message: string,
public code: PaymentErrorCode,
public signature?: string
) {
super(message);
this.name = 'PaymentError';
}
}
enum PaymentErrorCode {
TIMEOUT = 'TIMEOUT',
INSUFFICIENT_AMOUNT = 'INSUFFICIENT_AMOUNT',
TRANSACTION_FAILED = 'TRANSACTION_FAILED',
REORG_DETECTED = 'REORG_DETECTED',
NETWORK_ERROR = 'NETWORK_ERROR',
}
// Enhanced error handling
async function processPaymentWithRetry(
processor: PaymentProcessor,
config: PaymentConfig,
maxRetries: number = 3
): Promise<PaymentResult> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await processor.processPayment(config);
} catch (error) {
lastError = error as Error;
console.error(`Payment attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
const backoff = Math.pow(2, attempt) * 1000;
console.log(`Retrying in ${backoff}ms...`);
await new Promise(resolve => setTimeout(resolve, backoff));
}
}
}
throw new PaymentError(
`Payment failed after ${maxRetries} attempts: ${lastError?.message}`,
PaymentErrorCode.NETWORK_ERROR
);
}Performance Considerations
- WebSocket over HTTP: Use WebSocket subscriptions for real-time updates instead of polling
- Confirmation Strategy: Balance security (more confirmations) vs speed (fewer confirmations)
- Connection Pooling: Reuse connections for multiple payments
- Batch Verification: Check multiple transactions in a single
getSignatureStatusescall
Cost Optimization
| Operation | Credits/Call | Frequency | Optimization |
|---|---|---|---|
onAccountChange | 10/second | Continuous | Single subscription per address |
getTransaction | 10 | Per payment | Cache verified transactions |
getSignatureStatuses | 10 | Polling | Batch multiple signatures |
getSignaturesForAddress | 10 | Per change | Limit to recent signatures |
Estimated cost per payment: 50-100 credits (depending on confirmation time)
NFT Marketplace Integration
Build a complete NFT marketplace that queries user collections, displays metadata, monitors listings, and updates in real-time.
Use Case
Create a marketplace dashboard showing user NFTs, available listings, and real-time sale notifications.
Architecture
graph TB
A[User Wallet] --> B[Get Token Accounts]
B --> C[Filter NFTs]
C --> D[Fetch Metadata]
D --> E[Display Collection]
F[Marketplace Program] --> G[Get Program Accounts]
G --> H[Parse Listings]
H --> I[Display Marketplace]
J[WebSocket] --> K[Account Updates]
K --> L[Sale Events]
L --> M[UI Updates]
E --> N[Combined View]
I --> N
M --> NComplete Implementation
import { Connection, PublicKey, AccountInfo } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import axios from 'axios';
interface NFTMetadata {
mint: string;
name: string;
symbol: string;
uri: string;
image?: string;
attributes?: Array<{ trait_type: string; value: string }>;
}
interface MarketplaceListing {
mint: string;
seller: string;
price: number;
listingAccount: string;
}
class NFTMarketplace {
private connection: Connection;
private wsConnection: Connection;
private marketplaceProgramId: PublicKey;
constructor(rpcUrl: string, wsUrl: string, marketplaceProgramId: string) {
this.connection = new Connection(rpcUrl, 'confirmed');
this.wsConnection = new Connection(wsUrl, {
commitment: 'confirmed',
wsEndpoint: wsUrl,
});
this.marketplaceProgramId = new PublicKey(marketplaceProgramId);
}
/**
* Get all NFTs owned by a wallet
*/
async getUserNFTs(walletAddress: string): Promise<NFTMetadata[]> {
const wallet = new PublicKey(walletAddress);
console.log('Fetching token accounts...');
const tokenAccounts = await this.connection.getTokenAccountsByOwner(
wallet,
{ programId: TOKEN_PROGRAM_ID }
);
console.log(`Found ${tokenAccounts.value.length} token accounts`);
// Filter for NFTs (amount = 1, decimals = 0)
const nftMints: string[] = [];
for (const { account } of tokenAccounts.value) {
const data = account.data;
// Parse token account data
const amount = data.readBigUInt64LE(64);
const decimals = data.readUInt8(44);
if (amount === 1n && decimals === 0) {
const mint = new PublicKey(data.slice(0, 32));
nftMints.push(mint.toString());
}
}
console.log(`Found ${nftMints.length} NFTs`);
// Fetch metadata for all NFTs
return await this.batchFetchMetadata(nftMints);
}
/**
* Fetch metadata for multiple NFTs efficiently
*/
private async batchFetchMetadata(mints: string[]): Promise<NFTMetadata[]> {
const BATCH_SIZE = 10;
const results: NFTMetadata[] = [];
for (let i = 0; i < mints.length; i += BATCH_SIZE) {
const batch = mints.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.allSettled(
batch.map(mint => this.fetchNFTMetadata(mint))
);
for (const result of batchResults) {
if (result.status === 'fulfilled' && result.value) {
results.push(result.value);
}
}
// Rate limiting
if (i + BATCH_SIZE < mints.length) {
await this.delay(100);
}
}
return results;
}
/**
* Fetch metadata for a single NFT using multiple methods
*/
private async fetchNFTMetadata(mint: string): Promise<NFTMetadata | null> {
try {
// Method 1: Try Metaplex standard
const metadata = await this.getMetaplexMetadata(mint);
if (metadata) return metadata;
// Method 2: Try direct URI from account data
const directMetadata = await this.getDirectMetadata(mint);
if (directMetadata) return directMetadata;
// Method 3: Fallback to basic info
return {
mint,
name: `NFT ${mint.slice(0, 8)}`,
symbol: 'NFT',
uri: '',
};
} catch (error) {
console.error(`Error fetching metadata for ${mint}:`, error);
return null;
}
}
/**
* Get metadata using Metaplex standard
*/
private async getMetaplexMetadata(mint: string): Promise<NFTMetadata | null> {
try {
const METADATA_PROGRAM_ID = new PublicKey(
'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
);
// Derive metadata PDA
const [metadataPDA] = PublicKey.findProgramAddressSync(
[
Buffer.from('metadata'),
METADATA_PROGRAM_ID.toBuffer(),
new PublicKey(mint).toBuffer(),
],
METADATA_PROGRAM_ID
);
const accountInfo = await this.connection.getAccountInfo(metadataPDA);
if (!accountInfo) return null;
// Parse Metaplex metadata (simplified)
const data = accountInfo.data;
// Skip header and read name
let offset = 1 + 32 + 32; // key + update authority + mint
const nameLength = data.readUInt32LE(offset);
offset += 4;
const name = data.slice(offset, offset + nameLength).toString('utf8').replace(/\0/g, '');
offset += nameLength;
const symbolLength = data.readUInt32LE(offset);
offset += 4;
const symbol = data.slice(offset, offset + symbolLength).toString('utf8').replace(/\0/g, '');
offset += symbolLength;
const uriLength = data.readUInt32LE(offset);
offset += 4;
const uri = data.slice(offset, offset + uriLength).toString('utf8').replace(/\0/g, '');
// Fetch off-chain metadata
let image: string | undefined;
let attributes: Array<{ trait_type: string; value: string }> | undefined;
if (uri) {
try {
const response = await axios.get(uri, { timeout: 5000 });
image = response.data.image;
attributes = response.data.attributes;
} catch (error) {
console.warn(`Failed to fetch off-chain metadata from ${uri}`);
}
}
return { mint, name, symbol, uri, image, attributes };
} catch (error) {
return null;
}
}
/**
* Get metadata directly from mint account
*/
private async getDirectMetadata(mint: string): Promise<NFTMetadata | null> {
try {
const mintPubkey = new PublicKey(mint);
const accountInfo = await this.connection.getAccountInfo(mintPubkey);
if (!accountInfo) return null;
// Basic mint info
return {
mint,
name: `Token ${mint.slice(0, 8)}`,
symbol: 'TOKEN',
uri: '',
};
} catch (error) {
return null;
}
}
/**
* Get all marketplace listings
*/
async getAllListings(): Promise<MarketplaceListing[]> {
console.log('Fetching marketplace listings...');
const accounts = await this.connection.getProgramAccounts(
this.marketplaceProgramId,
{
filters: [
{ dataSize: 120 }, // Adjust based on your listing account size
],
}
);
console.log(`Found ${accounts.length} listing accounts`);
const listings: MarketplaceListing[] = [];
for (const { pubkey, account } of accounts) {
try {
const listing = this.parseListingAccount(account.data, pubkey.toString());
if (listing) {
listings.push(listing);
}
} catch (error) {
console.error(`Error parsing listing ${pubkey.toString()}:`, error);
}
}
return listings;
}
/**
* Parse listing account data
*/
private parseListingAccount(data: Buffer, pubkey: string): MarketplaceListing | null {
try {
// Adjust parsing based on your marketplace program structure
// This is a generic example
const mint = new PublicKey(data.slice(0, 32)).toString();
const seller = new PublicKey(data.slice(32, 64)).toString();
const price = Number(data.readBigUInt64LE(64)) / 1e9; // Convert lamports to SOL
return {
mint,
seller,
price,
listingAccount: pubkey,
};
} catch (error) {
return null;
}
}
/**
* Monitor marketplace sales in real-time
*/
async monitorSales(callback: (sale: SaleEvent) => void): Promise<number> {
const subscriptionId = this.wsConnection.onProgramAccountChange(
this.marketplaceProgramId,
async (keyedAccountInfo) => {
try {
const { accountId, accountInfo } = keyedAccountInfo;
// Check if account was closed (sale completed)
if (accountInfo.lamports === 0) {
const sale: SaleEvent = {
listingAccount: accountId.toString(),
timestamp: Date.now(),
type: 'sale_completed',
};
callback(sale);
} else {
// Check for new listing
const listing = this.parseListingAccount(
accountInfo.data,
accountId.toString()
);
if (listing) {
const sale: SaleEvent = {
listingAccount: accountId.toString(),
timestamp: Date.now(),
type: 'new_listing',
listing,
};
callback(sale);
}
}
} catch (error) {
console.error('Error processing sale event:', error);
}
},
'confirmed'
);
console.log('Monitoring marketplace sales...');
return subscriptionId;
}
/**
* Get marketplace stats
*/
async getMarketplaceStats(): Promise<MarketplaceStats> {
const listings = await this.getAllListings();
const totalListings = listings.length;
const floorPrice = Math.min(...listings.map(l => l.price));
const averagePrice = listings.reduce((sum, l) => sum + l.price, 0) / totalListings;
const totalVolume = listings.reduce((sum, l) => sum + l.price, 0);
return {
totalListings,
floorPrice,
averagePrice,
totalVolume,
};
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface SaleEvent {
listingAccount: string;
timestamp: number;
type: 'sale_completed' | 'new_listing';
listing?: MarketplaceListing;
}
interface MarketplaceStats {
totalListings: number;
floorPrice: number;
averagePrice: number;
totalVolume: number;
}
// Usage example
async function exampleNFTMarketplace() {
const marketplace = new NFTMarketplace(
'https://rpc.solana.com',
'wss://rpc.solana.com',
'YourMarketplaceProgramIdHere'
);
// Get user's NFTs
const nfts = await marketplace.getUserNFTs('UserWalletAddressHere');
console.log('User NFTs:', nfts);
// Get all listings
const listings = await marketplace.getAllListings();
console.log('Marketplace listings:', listings);
// Get stats
const stats = await marketplace.getMarketplaceStats();
console.log('Marketplace stats:', stats);
// Monitor sales
const subscriptionId = await marketplace.monitorSales((sale) => {
console.log('Sale event:', sale);
if (sale.type === 'sale_completed') {
console.log('NFT sold!', sale.listingAccount);
} else {
console.log('New listing:', sale.listing);
}
});
// Cleanup
// marketplace.wsConnection.removeProgramAccountChangeListener(subscriptionId);
}Performance Considerations
- Batch Metadata Fetches: Process multiple NFTs concurrently with rate limiting
- Cache Metadata: Store off-chain metadata locally to avoid repeated fetches
- Filter Early: Use
getTokenAccountsByOwnerfilters to reduce data transfer - Lazy Loading: Load NFT images progressively in UI
Cost Optimization
| Operation | Credits/Call | Optimization |
|---|---|---|
getTokenAccountsByOwner | 100 | Cache results, refresh periodically |
getAccountInfo (per NFT) | 10 | Batch requests, use multicall |
getProgramAccounts | 100 | Use filters to reduce data |
onProgramAccountChange | 10/second | Single subscription for all listings |
Validator Monitoring Dashboard
Monitor validator performance, network topology, and create alerts for issues.
Use Case
Build a comprehensive dashboard for validator operators to track performance and network health.
Architecture
graph LR
A[Dashboard] --> B[Vote Accounts]
A --> C[Performance Samples]
A --> D[Cluster Nodes]
B --> E[Validator Stats]
C --> E
D --> E
E --> F[Performance Trends]
E --> G[Network Topology]
E --> H[Alert System]
H --> I[Email/SMS]
H --> J[Webhook]Complete Implementation
import { Connection, VoteAccountStatus, PerfSample, ContactInfo } from '@solana/web3.js';
interface ValidatorMetrics {
identity: string;
voteAccount: string;
commission: number;
activatedStake: number;
lastVote: number;
rootSlot: number;
credits: number;
epochCredits: number;
skipRate: number;
performance: number[];
}
interface AlertConfig {
skipRateThreshold: number;
performanceThreshold: number;
delinquentAlert: boolean;
}
class ValidatorMonitor {
private connection: Connection;
private alertConfig: AlertConfig;
private metrics: Map<string, ValidatorMetrics>;
constructor(rpcUrl: string, alertConfig: AlertConfig) {
this.connection = new Connection(rpcUrl, 'confirmed');
this.alertConfig = alertConfig;
this.metrics = new Map();
}
/**
* Get complete validator information
*/
async getValidatorInfo(voteAccount: string): Promise<ValidatorMetrics | null> {
try {
// Get vote accounts
const voteAccounts = await this.connection.getVoteAccounts();
// Find specific validator
const validator = [...voteAccounts.current, ...voteAccounts.delinquent].find(
v => v.votePubkey === voteAccount
);
if (!validator) {
console.warn(`Validator ${voteAccount} not found`);
return null;
}
// Get performance samples
const perfSamples = await this.connection.getRecentPerformanceSamples(10);
const avgPerformance = this.calculatePerformance(perfSamples);
// Calculate skip rate
const skipRate = this.calculateSkipRate(validator);
const metrics: ValidatorMetrics = {
identity: validator.nodePubkey,
voteAccount: validator.votePubkey,
commission: validator.commission,
activatedStake: validator.activatedStake / 1e9, // Convert to SOL
lastVote: validator.lastVote,
rootSlot: validator.rootSlot,
credits: validator.epochCredits.reduce((sum, [_, credits]) => sum + credits, 0),
epochCredits: validator.epochCredits[0]?.[1] || 0,
skipRate,
performance: avgPerformance,
};
return metrics;
} catch (error) {
console.error('Error fetching validator info:', error);
return null;
}
}
/**
* Get all validators with performance metrics
*/
async getAllValidators(): Promise<ValidatorMetrics[]> {
const voteAccounts = await this.connection.getVoteAccounts();
const perfSamples = await this.connection.getRecentPerformanceSamples(10);
const avgPerformance = this.calculatePerformance(perfSamples);
const allValidators = [...voteAccounts.current, ...voteAccounts.delinquent];
return allValidators.map(validator => ({
identity: validator.nodePubkey,
voteAccount: validator.votePubkey,
commission: validator.commission,
activatedStake: validator.activatedStake / 1e9,
lastVote: validator.lastVote,
rootSlot: validator.rootSlot,
credits: validator.epochCredits.reduce((sum, [_, credits]) => sum + credits, 0),
epochCredits: validator.epochCredits[0]?.[1] || 0,
skipRate: this.calculateSkipRate(validator),
performance: avgPerformance,
}));
}
/**
* Calculate skip rate for a validator
*/
private calculateSkipRate(validator: any): number {
const recentEpochs = validator.epochCredits.slice(0, 5);
if (recentEpochs.length < 2) return 0;
let totalSlots = 0;
let totalCredits = 0;
for (let i = 1; i < recentEpochs.length; i++) {
const [epoch, credits, previousCredits] = recentEpochs[i];
const [prevEpoch] = recentEpochs[i - 1];
const creditsEarned = credits - previousCredits;
const slotsInEpoch = 432000; // Approximate
totalCredits += creditsEarned;
totalSlots += slotsInEpoch;
}
const skipRate = ((totalSlots - totalCredits) / totalSlots) * 100;
return Math.max(0, Math.min(100, skipRate));
}
/**
* Calculate average performance from samples
*/
private calculatePerformance(samples: PerfSample[]): number[] {
return samples.map(sample => {
const slotsPerSecond = sample.numSlots / sample.samplePeriodSecs;
const transactionsPerSecond = sample.numTransactions / sample.samplePeriodSecs;
return transactionsPerSecond;
});
}
/**
* Get network topology
*/
async getNetworkTopology(): Promise<NetworkTopology> {
const clusterNodes = await this.connection.getClusterNodes();
const topology: NetworkTopology = {
totalNodes: clusterNodes.length,
nodesByVersion: {},
nodesByRegion: {},
nodes: clusterNodes.map(node => ({
pubkey: node.pubkey,
gossip: node.gossip,
tpu: node.tpu,
rpc: node.rpc || null,
version: node.version || 'unknown',
})),
};
// Group by version
for (const node of clusterNodes) {
const version = node.version || 'unknown';
topology.nodesByVersion[version] = (topology.nodesByVersion[version] || 0) + 1;
}
return topology;
}
/**
* Monitor validator and trigger alerts
*/
async monitorValidator(
voteAccount: string,
callback: (alert: ValidatorAlert) => void
): Promise<void> {
const checkInterval = 60000; // 1 minute
const check = async () => {
try {
const metrics = await this.getValidatorInfo(voteAccount);
if (!metrics) {
callback({
type: 'error',
message: 'Failed to fetch validator metrics',
timestamp: Date.now(),
voteAccount,
});
return;
}
// Check skip rate
if (metrics.skipRate > this.alertConfig.skipRateThreshold) {
callback({
type: 'skip_rate',
message: `Skip rate ${metrics.skipRate.toFixed(2)}% exceeds threshold ${this.alertConfig.skipRateThreshold}%`,
timestamp: Date.now(),
voteAccount,
metrics,
});
}
// Check if delinquent
const voteAccounts = await this.connection.getVoteAccounts();
const isDelinquent = voteAccounts.delinquent.some(
v => v.votePubkey === voteAccount
);
if (isDelinquent && this.alertConfig.delinquentAlert) {
callback({
type: 'delinquent',
message: 'Validator is delinquent',
timestamp: Date.now(),
voteAccount,
metrics,
});
}
// Store metrics for trend analysis
this.metrics.set(voteAccount, metrics);
} catch (error) {
console.error('Error monitoring validator:', error);
callback({
type: 'error',
message: `Monitoring error: ${error}`,
timestamp: Date.now(),
voteAccount,
});
}
};
// Initial check
await check();
// Start monitoring
setInterval(check, checkInterval);
}
/**
* Generate performance report
*/
async generateReport(voteAccount: string): Promise<ValidatorReport> {
const metrics = await this.getValidatorInfo(voteAccount);
if (!metrics) {
throw new Error('Failed to fetch validator metrics');
}
const voteAccounts = await this.connection.getVoteAccounts();
const allValidators = [...voteAccounts.current, ...voteAccounts.delinquent];
// Calculate percentiles
const skipRates = allValidators.map(v => this.calculateSkipRate(v));
const stakes = allValidators.map(v => v.activatedStake);
skipRates.sort((a, b) => a - b);
stakes.sort((a, b) => a - b);
const skipRatePercentile = this.getPercentile(skipRates, metrics.skipRate);
const stakePercentile = this.getPercentile(stakes, metrics.activatedStake * 1e9);
return {
metrics,
skipRatePercentile,
stakePercentile,
rank: this.calculateRank(metrics, allValidators),
status: this.getValidatorStatus(metrics),
};
}
private getPercentile(sortedArray: number[], value: number): number {
const index = sortedArray.findIndex(v => v >= value);
return (index / sortedArray.length) * 100;
}
private calculateRank(metrics: ValidatorMetrics, allValidators: any[]): number {
const sorted = allValidators.sort((a, b) => b.activatedStake - a.activatedStake);
return sorted.findIndex(v => v.votePubkey === metrics.voteAccount) + 1;
}
private getValidatorStatus(metrics: ValidatorMetrics): string {
if (metrics.skipRate > 10) return 'poor';
if (metrics.skipRate > 5) return 'fair';
if (metrics.skipRate > 2) return 'good';
return 'excellent';
}
}
interface NetworkTopology {
totalNodes: number;
nodesByVersion: Record<string, number>;
nodesByRegion: Record<string, number>;
nodes: Array<{
pubkey: string;
gossip: string | null;
tpu: string | null;
rpc: string | null;
version: string;
}>;
}
interface ValidatorAlert {
type: 'skip_rate' | 'delinquent' | 'performance' | 'error';
message: string;
timestamp: number;
voteAccount: string;
metrics?: ValidatorMetrics;
}
interface ValidatorReport {
metrics: ValidatorMetrics;
skipRatePercentile: number;
stakePercentile: number;
rank: number;
status: string;
}
// Usage example
async function exampleValidatorMonitoring() {
const monitor = new ValidatorMonitor(
'https://rpc.solana.com',
{
skipRateThreshold: 5,
performanceThreshold: 0.8,
delinquentAlert: true,
}
);
// Get validator info
const info = await monitor.getValidatorInfo('VoteAccountAddressHere');
console.log('Validator info:', info);
// Get all validators
const allValidators = await monitor.getAllValidators();
console.log(`Total validators: ${allValidators.length}`);
// Get network topology
const topology = await monitor.getNetworkTopology();
console.log('Network topology:', topology);
// Monitor with alerts
await monitor.monitorValidator('VoteAccountAddressHere', (alert) => {
console.log('ALERT:', alert);
// Send notification (email, webhook, etc.)
if (alert.type === 'delinquent') {
// Send urgent alert
sendUrgentNotification(alert);
}
});
// Generate report
const report = await monitor.generateReport('VoteAccountAddressHere');
console.log('Validator report:', report);
}
function sendUrgentNotification(alert: ValidatorAlert): void {
// Implement notification logic
console.log('URGENT:', alert.message);
}Performance Considerations
- Caching: Cache vote accounts and update every epoch
- Batch Queries: Combine multiple validator queries
- Historical Data: Store performance samples in database for trend analysis
- Alert Throttling: Prevent alert spam with cooldown periods
Cost Optimization
| Operation | Credits/Call | Frequency | Optimization |
|---|---|---|---|
getVoteAccounts | 100 | Per epoch | Cache for 1+ hours |
getRecentPerformanceSamples | 10 | Every minute | Batch with other calls |
getClusterNodes | 10 | Hourly | Cache topology data |
DeFi Position Tracker
Track wallet positions across multiple DeFi protocols, calculate USD values, and monitor changes.
Use Case
Build a portfolio tracker that shows users their positions across lending platforms, DEXs, and yield farms.
Architecture
graph TB
A[Wallet] --> B[Protocol 1]
A --> C[Protocol 2]
A --> D[Protocol 3]
B --> E[Get Program Accounts]
C --> E
D --> E
E --> F[Parse Positions]
F --> G[Fetch Token Prices]
G --> H[Calculate USD Values]
H --> I[Portfolio Dashboard]
J[WebSocket] --> K[Position Updates]
K --> L[Rebalance Alerts]Complete Implementation
import { Connection, PublicKey } from '@solana/web3.js';
import axios from 'axios';
interface Position {
protocol: string;
type: 'lending' | 'liquidity' | 'staking' | 'vault';
asset: string;
amount: number;
usdValue: number;
apy?: number;
health?: number;
}
interface ProtocolConfig {
name: string;
programId: string;
parser: (data: Buffer) => any;
}
class DeFiPositionTracker {
private connection: Connection;
private wsConnection: Connection;
private protocols: ProtocolConfig[];
private priceCache: Map<string, { price: number; timestamp: number }>;
constructor(rpcUrl: string, wsUrl: string) {
this.connection = new Connection(rpcUrl, 'confirmed');
this.wsConnection = new Connection(wsUrl, {
commitment: 'confirmed',
wsEndpoint: wsUrl,
});
this.protocols = this.initializeProtocols();
this.priceCache = new Map();
}
/**
* Initialize supported protocols
*/
private initializeProtocols(): ProtocolConfig[] {
return [
{
name: 'Marinade',
programId: 'MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD',
parser: this.parseMarinadePosition,
},
{
name: 'Orca',
programId: 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc',
parser: this.parseOrcaPosition,
},
// Add more protocols
];
}
/**
* Get all positions for a wallet
*/
async getAllPositions(walletAddress: string): Promise<Position[]> {
const wallet = new PublicKey(walletAddress);
const allPositions: Position[] = [];
console.log('Fetching positions across all protocols...');
for (const protocol of this.protocols) {
try {
const positions = await this.getProtocolPositions(wallet, protocol);
allPositions.push(...positions);
} catch (error) {
console.error(`Error fetching ${protocol.name} positions:`, error);
}
}
// Calculate USD values
await this.enrichWithPrices(allPositions);
return allPositions;
}
/**
* Get positions for a specific protocol
*/
private async getProtocolPositions(
wallet: PublicKey,
protocol: ProtocolConfig
): Promise<Position[]> {
const programId = new PublicKey(protocol.programId);
// Get all program accounts for this wallet
const accounts = await this.connection.getProgramAccounts(programId, {
filters: [
{
memcmp: {
offset: 8, // Skip discriminator
bytes: wallet.toBase58(),
},
},
],
});
console.log(`Found ${accounts.length} ${protocol.name} accounts`);
const positions: Position[] = [];
for (const { account } of accounts) {
try {
const parsed = protocol.parser(account.data);
if (parsed) {
positions.push({
protocol: protocol.name,
...parsed,
usdValue: 0, // Will be calculated later
});
}
} catch (error) {
console.error(`Error parsing ${protocol.name} account:`, error);
}
}
return positions;
}
/**
* Parse Marinade stake position
*/
private parseMarinadePosition(data: Buffer): Partial<Position> | null {
try {
// Simplified parsing - adjust based on actual program structure
const amount = Number(data.readBigUInt64LE(40)) / 1e9;
return {
type: 'staking',
asset: 'mSOL',
amount,
apy: 7.5, // Would fetch from API
};
} catch (error) {
return null;
}
}
/**
* Parse Orca liquidity position
*/
private parseOrcaPosition(data: Buffer): Partial<Position> | null {
try {
// Simplified parsing
const liquidity = Number(data.readBigUInt64LE(65));
return {
type: 'liquidity',
asset: 'LP-Token',
amount: liquidity / 1e9,
apy: 15.2, // Would fetch from API
};
} catch (error) {
return null;
}
}
/**
* Enrich positions with USD prices
*/
private async enrichWithPrices(positions: Position[]): Promise<void> {
const uniqueAssets = [...new Set(positions.map(p => p.asset))];
// Fetch prices for all unique assets
const prices = await this.fetchPrices(uniqueAssets);
// Update positions with USD values
for (const position of positions) {
const price = prices.get(position.asset) || 0;
position.usdValue = position.amount * price;
}
}
/**
* Fetch token prices from Jupiter or CoinGecko
*/
private async fetchPrices(assets: string[]): Promise<Map<string, number>> {
const prices = new Map<string, number>();
const CACHE_DURATION = 60000; // 1 minute
for (const asset of assets) {
// Check cache
const cached = this.priceCache.get(asset);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
prices.set(asset, cached.price);
continue;
}
try {
// Fetch from Jupiter Price API
const response = await axios.get(
`https://price.jup.ag/v4/price?ids=${asset}`,
{ timeout: 5000 }
);
const price = response.data.data[asset]?.price || 0;
prices.set(asset, price);
// Cache price
this.priceCache.set(asset, { price, timestamp: Date.now() });
} catch (error) {
console.error(`Error fetching price for ${asset}:`, error);
prices.set(asset, 0);
}
}
return prices;
}
/**
* Calculate portfolio summary
*/
calculatePortfolioSummary(positions: Position[]): PortfolioSummary {
const totalValue = positions.reduce((sum, p) => sum + p.usdValue, 0);
const byProtocol: Record<string, number> = {};
const byType: Record<string, number> = {};
for (const position of positions) {
byProtocol[position.protocol] = (byProtocol[position.protocol] || 0) + position.usdValue;
byType[position.type] = (byType[position.type] || 0) + position.usdValue;
}
// Calculate weighted average APY
const totalApy = positions
.filter(p => p.apy)
.reduce((sum, p) => sum + (p.apy! * p.usdValue), 0);
const weightedApy = totalValue > 0 ? totalApy / totalValue : 0;
return {
totalValue,
byProtocol,
byType,
weightedApy,
positions: positions.length,
};
}
/**
* Monitor positions and trigger alerts
*/
async monitorPositions(
walletAddress: string,
alertConfig: AlertConfig,
callback: (alert: PositionAlert) => void
): Promise<void> {
const wallet = new PublicKey(walletAddress);
const subscriptions: number[] = [];
for (const protocol of this.protocols) {
const programId = new PublicKey(protocol.programId);
const subscriptionId = this.wsConnection.onProgramAccountChange(
programId,
async (keyedAccountInfo) => {
try {
const { accountId, accountInfo } = keyedAccountInfo;
// Check if account belongs to our wallet
const accountData = accountInfo.data;
// Simplified check - adjust based on program structure
const ownerPubkey = new PublicKey(accountData.slice(8, 40));
if (!ownerPubkey.equals(wallet)) return;
// Parse position
const parsed = protocol.parser(accountData);
if (!parsed) return;
const position: Position = {
protocol: protocol.name,
...parsed,
usdValue: 0,
};
// Enrich with price
await this.enrichWithPrices([position]);
// Check alerts
this.checkAlerts(position, alertConfig, callback);
} catch (error) {
console.error('Error monitoring position:', error);
}
},
'confirmed',
[
{
memcmp: {
offset: 8,
bytes: wallet.toBase58(),
},
},
]
);
subscriptions.push(subscriptionId);
}
console.log(`Monitoring ${subscriptions.length} protocols for position changes...`);
}
/**
* Check if position triggers any alerts
*/
private checkAlerts(
position: Position,
config: AlertConfig,
callback: (alert: PositionAlert) => void
): void {
// Health factor alert (for lending positions)
if (position.health && position.health < config.healthFactorThreshold) {
callback({
type: 'health_factor',
message: `Health factor ${position.health.toFixed(2)} below threshold`,
position,
timestamp: Date.now(),
});
}
// Large value change alert
// Would need to track previous values for this
// Rebalancing opportunity
if (position.apy && position.apy > config.rebalanceApyThreshold) {
callback({
type: 'rebalance_opportunity',
message: `High APY opportunity: ${position.apy.toFixed(2)}%`,
position,
timestamp: Date.now(),
});
}
}
/**
* Export positions to CSV
*/
exportToCSV(positions: Position[]): string {
const headers = ['Protocol', 'Type', 'Asset', 'Amount', 'USD Value', 'APY', 'Health'];
const rows = positions.map(p => [
p.protocol,
p.type,
p.asset,
p.amount.toFixed(6),
p.usdValue.toFixed(2),
p.apy?.toFixed(2) || '',
p.health?.toFixed(2) || '',
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
}
interface PortfolioSummary {
totalValue: number;
byProtocol: Record<string, number>;
byType: Record<string, number>;
weightedApy: number;
positions: number;
}
interface AlertConfig {
healthFactorThreshold: number;
rebalanceApyThreshold: number;
valueChangeThreshold: number;
}
interface PositionAlert {
type: 'health_factor' | 'value_change' | 'rebalance_opportunity';
message: string;
position: Position;
timestamp: number;
}
// Usage example
async function exampleDeFiTracking() {
const tracker = new DeFiPositionTracker(
'https://rpc.solana.com',
'wss://rpc.solana.com'
);
// Get all positions
const positions = await tracker.getAllPositions('WalletAddressHere');
console.log('All positions:', positions);
// Calculate summary
const summary = tracker.calculatePortfolioSummary(positions);
console.log('Portfolio summary:', summary);
console.log(`Total value: $${summary.totalValue.toFixed(2)}`);
console.log(`Weighted APY: ${summary.weightedApy.toFixed(2)}%`);
// Export to CSV
const csv = tracker.exportToCSV(positions);
console.log('CSV export:', csv);
// Monitor positions
await tracker.monitorPositions(
'WalletAddressHere',
{
healthFactorThreshold: 1.5,
rebalanceApyThreshold: 20,
valueChangeThreshold: 1000,
},
(alert) => {
console.log('POSITION ALERT:', alert);
}
);
}Performance Considerations
- Parallel Queries: Fetch positions from multiple protocols concurrently
- Price Caching: Cache token prices to reduce API calls
- Incremental Updates: Use WebSocket to update only changed positions
- Database Storage: Store historical positions for trend analysis
Cost Optimization
| Operation | Credits/Call | Frequency | Optimization |
|---|---|---|---|
getProgramAccounts (per protocol) | 100 | On load | Cache and use WebSocket for updates |
| Price API calls | External | Per unique asset | Cache for 1 minute |
onProgramAccountChange | 10/second | Continuous | Single subscription per protocol |
Transaction History Export
Efficiently export complete transaction history with pagination, parsing, and categorization.
Use Case
Export a wallet's complete transaction history for tax reporting, accounting, or analysis.
Architecture
graph LR
A[Wallet Address] --> B[getSignaturesForAddress]
B --> C[Paginate Results]
C --> D[Batch getTransaction]
D --> E[Parse & Categorize]
E --> F[Export CSV/JSON]
G[Progress Tracking] --> C
H[Error Handling] --> D
I[Rate Limiting] --> DComplete Implementation
import { Connection, PublicKey, ParsedTransactionWithMeta } from '@solana/web3.js';
import * as fs from 'fs';
interface TransactionRecord {
signature: string;
timestamp: number;
type: string;
from: string;
to: string;
amount: number;
fee: number;
status: 'success' | 'failed';
memo?: string;
}
interface ExportOptions {
startTime?: number;
endTime?: number;
format: 'csv' | 'json';
outputPath: string;
batchSize: number;
includeFailedTx: boolean;
}
class TransactionHistoryExporter {
private connection: Connection;
constructor(rpcUrl: string) {
this.connection = new Connection(rpcUrl, 'confirmed');
}
/**
* Export complete transaction history
*/
async exportHistory(
walletAddress: string,
options: ExportOptions
): Promise<ExportResult> {
const wallet = new PublicKey(walletAddress);
const startTime = Date.now();
console.log('Starting transaction history export...');
console.log('Options:', options);
// Step 1: Get all signatures with pagination
const signatures = await this.getAllSignatures(wallet, options);
console.log(`Found ${signatures.length} transactions`);
// Step 2: Fetch transaction details in batches
const transactions = await this.batchFetchTransactions(signatures, options.batchSize);
console.log(`Fetched ${transactions.length} transaction details`);
// Step 3: Parse and categorize transactions
const records = this.parseTransactions(transactions, walletAddress);
console.log(`Parsed ${records.length} transaction records`);
// Step 4: Filter by time range
const filtered = this.filterByTimeRange(records, options);
console.log(`After filtering: ${filtered.length} records`);
// Step 5: Export to file
await this.exportToFile(filtered, options);
const duration = Date.now() - startTime;
return {
totalTransactions: filtered.length,
successfulTx: filtered.filter(r => r.status === 'success').length,
failedTx: filtered.filter(r => r.status === 'failed').length,
totalFees: filtered.reduce((sum, r) => sum + r.fee, 0),
duration,
outputPath: options.outputPath,
};
}
/**
* Get all signatures with pagination
*/
private async getAllSignatures(
wallet: PublicKey,
options: ExportOptions
): Promise<string[]> {
const allSignatures: string[] = [];
let before: string | undefined;
let hasMore = true;
while (hasMore) {
try {
const signatures = await this.connection.getSignaturesForAddress(
wallet,
{ before, limit: 1000 },
'confirmed'
);
if (signatures.length === 0) {
hasMore = false;
break;
}
// Filter by time if specified
for (const sig of signatures) {
if (options.startTime && sig.blockTime && sig.blockTime < options.startTime) {
hasMore = false;
break;
}
// Skip failed transactions if not requested
if (!options.includeFailedTx && sig.err) {
continue;
}
allSignatures.push(sig.signature);
}
before = signatures[signatures.length - 1].signature;
console.log(`Fetched ${allSignatures.length} signatures so far...`);
// Rate limiting
await this.delay(100);
} catch (error) {
console.error('Error fetching signatures:', error);
break;
}
}
return allSignatures;
}
/**
* Batch fetch transaction details
*/
private async batchFetchTransactions(
signatures: string[],
batchSize: number
): Promise<(ParsedTransactionWithMeta | null)[]> {
const transactions: (ParsedTransactionWithMeta | null)[] = [];
for (let i = 0; i < signatures.length; i += batchSize) {
const batch = signatures.slice(i, i + batchSize);
console.log(`Fetching batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(signatures.length / batchSize)}`);
try {
// Fetch transactions concurrently within batch
const batchPromises = batch.map(sig =>
this.connection.getParsedTransaction(sig, {
maxSupportedTransactionVersion: 0,
commitment: 'confirmed',
})
);
const batchResults = await Promise.allSettled(batchPromises);
for (const result of batchResults) {
if (result.status === 'fulfilled') {
transactions.push(result.value);
} else {
console.error('Failed to fetch transaction:', result.reason);
transactions.push(null);
}
}
// Rate limiting between batches
if (i + batchSize < signatures.length) {
await this.delay(500);
}
} catch (error) {
console.error('Error in batch fetch:', error);
}
}
return transactions;
}
/**
* Parse transactions into structured records
*/
private parseTransactions(
transactions: (ParsedTransactionWithMeta | null)[],
walletAddress: string
): TransactionRecord[] {
const records: TransactionRecord[] = [];
for (const tx of transactions) {
if (!tx) continue;
try {
const record = this.parseTransaction(tx, walletAddress);
if (record) {
records.push(record);
}
} catch (error) {
console.error('Error parsing transaction:', error);
}
}
return records;
}
/**
* Parse a single transaction
*/
private parseTransaction(
tx: ParsedTransactionWithMeta,
walletAddress: string
): TransactionRecord | null {
const signature = tx.transaction.signatures[0];
const timestamp = tx.blockTime || 0;
const status = tx.meta?.err ? 'failed' : 'success';
const fee = (tx.meta?.fee || 0) / 1e9;
// Determine transaction type
let type = 'unknown';
let from = '';
let to = '';
let amount = 0;
let memo: string | undefined;
// Check for SPL token transfers
if (tx.meta?.postTokenBalances && tx.meta?.preTokenBalances) {
const postBalances = tx.meta.postTokenBalances;
const preBalances = tx.meta.preTokenBalances;
for (const postBalance of postBalances) {
const preBalance = preBalances.find(
pb => pb.accountIndex === postBalance.accountIndex
);
if (preBalance && postBalance.uiTokenAmount.uiAmount !== preBalance.uiTokenAmount.uiAmount) {
type = 'token_transfer';
amount = Math.abs(
(postBalance.uiTokenAmount.uiAmount || 0) - (preBalance.uiTokenAmount.uiAmount || 0)
);
break;
}
}
}
// Check for SOL transfers
if (type === 'unknown' && tx.meta?.postBalances && tx.meta?.preBalances) {
const accountKeys = tx.transaction.message.accountKeys;
for (let i = 0; i < accountKeys.length; i++) {
const preBalance = tx.meta.preBalances[i];
const postBalance = tx.meta.postBalances[i];
const change = (postBalance - preBalance) / 1e9;
if (Math.abs(change) > 0.000001) {
const account = accountKeys[i].pubkey.toString();
if (change < 0) {
from = account;
amount = Math.abs(change);
} else if (change > 0 && account !== walletAddress) {
to = account;
}
type = 'sol_transfer';
}
}
}
// Check for program interactions
if (type === 'unknown') {
const instructions = tx.transaction.message.instructions;
if (instructions.length > 0) {
// Simplified - would need more sophisticated parsing
type = 'program_interaction';
}
}
// Extract memo if present
const logMessages = tx.meta?.logMessages || [];
for (const log of logMessages) {
if (log.includes('Memo')) {
memo = log;
break;
}
}
return {
signature,
timestamp,
type,
from,
to,
amount,
fee,
status: status as 'success' | 'failed',
memo,
};
}
/**
* Filter records by time range
*/
private filterByTimeRange(
records: TransactionRecord[],
options: ExportOptions
): TransactionRecord[] {
return records.filter(record => {
if (options.startTime && record.timestamp < options.startTime) {
return false;
}
if (options.endTime && record.timestamp > options.endTime) {
return false;
}
return true;
});
}
/**
* Export records to file
*/
private async exportToFile(
records: TransactionRecord[],
options: ExportOptions
): Promise<void> {
if (options.format === 'csv') {
await this.exportToCSV(records, options.outputPath);
} else {
await this.exportToJSON(records, options.outputPath);
}
console.log(`Exported to ${options.outputPath}`);
}
/**
* Export to CSV format
*/
private async exportToCSV(records: TransactionRecord[], outputPath: string): Promise<void> {
const headers = ['Signature', 'Date', 'Type', 'From', 'To', 'Amount', 'Fee', 'Status', 'Memo'];
const rows = records.map(record => [
record.signature,
new Date(record.timestamp * 1000).toISOString(),
record.type,
record.from,
record.to,
record.amount.toFixed(9),
record.fee.toFixed(9),
record.status,
record.memo || '',
]);
const csv = [headers, ...rows]
.map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
fs.writeFileSync(outputPath, csv);
}
/**
* Export to JSON format
*/
private async exportToJSON(records: TransactionRecord[], outputPath: string): Promise<void> {
const json = JSON.stringify(records, null, 2);
fs.writeFileSync(outputPath, json);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface ExportResult {
totalTransactions: number;
successfulTx: number;
failedTx: number;
totalFees: number;
duration: number;
outputPath: string;
}
// Usage example
async function exampleExport() {
const exporter = new TransactionHistoryExporter('https://rpc.solana.com');
try {
const result = await exporter.exportHistory('WalletAddressHere', {
format: 'csv',
outputPath: '/tmp/transaction_history.csv',
batchSize: 10,
includeFailedTx: true,
// Optional time range (Unix timestamps)
// startTime: 1640000000,
// endTime: 1672535999,
});
console.log('Export complete!');
console.log(`Total transactions: ${result.totalTransactions}`);
console.log(`Successful: ${result.successfulTx}`);
console.log(`Failed: ${result.failedTx}`);
console.log(`Total fees: ${result.totalFees.toFixed(9)} SOL`);
console.log(`Duration: ${result.duration}ms`);
console.log(`Output: ${result.outputPath}`);
} catch (error) {
console.error('Export failed:', error);
}
}Performance Considerations
- Pagination Strategy: Fetch 1000 signatures per call (maximum)
- Batch Size: Balance between speed and rate limits (10-20 concurrent requests)
- Progress Tracking: Show progress for long-running exports
- Memory Management: Stream to file for very large exports
Cost Optimization
| Operation | Credits/Call | Total Calls | Optimization |
|---|---|---|---|
getSignaturesForAddress | 10 | N/1000 | Minimize by caching known signatures |
getParsedTransaction | 10 | N | Batch requests, handle failures gracefully |
Example cost for 10,000 transactions: ~100,100 credits (100 for signatures + 100,000 for transactions)
High-Frequency Trading Setup
Optimize for speed and reliability in high-frequency trading scenarios.
Use Case
Build a trading bot that executes trades based on market conditions with minimal latency.
Architecture
graph TB
A[Market Data] --> B[WebSocket Feeds]
B --> C[Price Analysis]
C --> D[Trading Decision]
D --> E[Connection Pool]
E --> F[Send Transaction]
G[Slot Updates] --> H[Timing Optimization]
H --> F
F --> I[Confirmation Monitor]
I --> J[Success/Retry]
K[Fallback RPC] --> EComplete Implementation
import {
Connection,
Keypair,
Transaction,
PublicKey,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
interface ConnectionConfig {
primary: string;
fallbacks: string[];
wsUrl: string;
maxConnectionsPerEndpoint: number;
}
interface TradingConfig {
maxSlippage: number;
priorityFee: number;
confirmationTimeout: number;
retryAttempts: number;
}
class HighFrequencyTrader {
private primaryPool: Connection[];
private fallbackPools: Map<string, Connection[]>;
private wsConnection: Connection;
private currentSlot: number = 0;
private config: TradingConfig;
constructor(connectionConfig: ConnectionConfig, tradingConfig: TradingConfig) {
this.config = tradingConfig;
// Initialize connection pools
this.primaryPool = this.createConnectionPool(
connectionConfig.primary,
connectionConfig.maxConnectionsPerEndpoint
);
this.fallbackPools = new Map();
for (const fallback of connectionConfig.fallbacks) {
this.fallbackPools.set(
fallback,
this.createConnectionPool(fallback, connectionConfig.maxConnectionsPerEndpoint)
);
}
// WebSocket connection for real-time updates
this.wsConnection = new Connection(connectionConfig.wsUrl, {
commitment: 'confirmed',
wsEndpoint: connectionConfig.wsUrl,
});
this.initializeSlotTracking();
}
/**
* Create a pool of connections to the same endpoint
*/
private createConnectionPool(url: string, size: number): Connection[] {
return Array(size)
.fill(null)
.map(() => new Connection(url, {
commitment: 'confirmed',
confirmTransactionInitialTimeout: this.config.confirmationTimeout,
}));
}
/**
* Get a connection from the pool (round-robin)
*/
private getConnection(preferFallback: boolean = false): Connection {
if (preferFallback && this.fallbackPools.size > 0) {
const [url, pool] = Array.from(this.fallbackPools.entries())[0];
const index = Math.floor(Math.random() * pool.length);
return pool[index];
}
const index = Math.floor(Math.random() * this.primaryPool.length);
return this.primaryPool[index];
}
/**
* Initialize slot tracking for timing optimization
*/
private initializeSlotTracking(): void {
this.wsConnection.onSlotChange((slotInfo) => {
this.currentSlot = slotInfo.slot;
});
}
/**
* Execute trade with optimized routing
*/
async executeTrade(
instruction: TransactionInstruction,
payer: Keypair
): Promise<TradeResult> {
const startTime = Date.now();
let attempts = 0;
let lastError: Error | null = null;
while (attempts < this.config.retryAttempts) {
attempts++;
try {
// Get fresh blockhash
const connection = this.getConnection();
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
// Build transaction
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash,
lastValidBlockHeight,
});
// Add priority fee
if (this.config.priorityFee > 0) {
transaction.add(
// Priority fee instruction would go here
// This is program-specific
);
}
transaction.add(instruction);
// Sign transaction
transaction.sign(payer);
// Send transaction using multiple strategies
const signature = await this.sendTransactionOptimized(transaction);
// Monitor confirmation
const confirmed = await this.waitForConfirmation(signature, lastValidBlockHeight);
if (confirmed) {
const duration = Date.now() - startTime;
return {
success: true,
signature,
slot: this.currentSlot,
duration,
attempts,
};
}
lastError = new Error('Transaction not confirmed');
} catch (error) {
lastError = error as Error;
console.error(`Trade attempt ${attempts} failed:`, error);
// Wait before retry with exponential backoff
if (attempts < this.config.retryAttempts) {
const backoff = Math.min(1000 * Math.pow(2, attempts), 5000);
await this.delay(backoff);
}
}
}
const duration = Date.now() - startTime;
return {
success: false,
error: lastError?.message || 'Unknown error',
duration,
attempts,
};
}
/**
* Send transaction using optimized strategies
*/
private async sendTransactionOptimized(transaction: Transaction): Promise<string> {
const serialized = transaction.serialize();
// Strategy 1: Send to multiple endpoints simultaneously
const sendPromises = [
this.getConnection().sendRawTransaction(serialized, {
skipPreflight: true,
maxRetries: 0,
}),
];
// Add fallback sends
for (const [_, pool] of this.fallbackPools) {
sendPromises.push(
pool[0].sendRawTransaction(serialized, {
skipPreflight: true,
maxRetries: 0,
})
);
}
// Race all sends, use first successful
const results = await Promise.allSettled(sendPromises);
for (const result of results) {
if (result.status === 'fulfilled') {
return result.value;
}
}
throw new Error('All transaction sends failed');
}
/**
* Wait for transaction confirmation with timeout
*/
private async waitForConfirmation(
signature: string,
lastValidBlockHeight: number
): Promise<boolean> {
const connection = this.getConnection();
const startTime = Date.now();
while (Date.now() - startTime < this.config.confirmationTimeout) {
try {
const status = await connection.getSignatureStatus(signature);
if (status.value?.confirmationStatus === 'confirmed' ||
status.value?.confirmationStatus === 'finalized') {
return true;
}
if (status.value?.err) {
console.error('Transaction failed:', status.value.err);
return false;
}
// Check if blockhash expired
const currentBlockHeight = await connection.getBlockHeight();
if (currentBlockHeight > lastValidBlockHeight) {
console.warn('Transaction expired');
return false;
}
await this.delay(400);
} catch (error) {
console.error('Error checking confirmation:', error);
await this.delay(400);
}
}
return false;
}
/**
* Batch multiple trades for efficiency
*/
async executeBatchTrades(
instructions: TransactionInstruction[],
payer: Keypair
): Promise<BatchTradeResult> {
const connection = this.getConnection();
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
// Split into multiple transactions if needed (max ~10 instructions per tx)
const INSTRUCTIONS_PER_TX = 8;
const transactions: Transaction[] = [];
for (let i = 0; i < instructions.length; i += INSTRUCTIONS_PER_TX) {
const batch = instructions.slice(i, i + INSTRUCTIONS_PER_TX);
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash,
lastValidBlockHeight,
});
batch.forEach(ix => transaction.add(ix));
transaction.sign(payer);
transactions.push(transaction);
}
// Send all transactions
const results = await Promise.allSettled(
transactions.map(tx => this.sendTransactionOptimized(tx))
);
const successful: string[] = [];
const failed: string[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push(result.reason.message);
}
}
return {
total: transactions.length,
successful: successful.length,
failed: failed.length,
signatures: successful,
};
}
/**
* Monitor market data in real-time
*/
subscribeToMarketData(
account: PublicKey,
callback: (data: MarketData) => void
): number {
return this.wsConnection.onAccountChange(
account,
(accountInfo, context) => {
const data: MarketData = {
slot: context.slot,
timestamp: Date.now(),
data: accountInfo.data,
lamports: accountInfo.lamports,
};
callback(data);
},
'confirmed'
);
}
/**
* Health check for all connections
*/
async healthCheck(): Promise<HealthStatus> {
const checks: Promise<boolean>[] = [];
// Check primary pool
for (const conn of this.primaryPool) {
checks.push(
conn.getSlot()
.then(() => true)
.catch(() => false)
);
}
// Check fallback pools
for (const [_, pool] of this.fallbackPools) {
for (const conn of pool) {
checks.push(
conn.getSlot()
.then(() => true)
.catch(() => false)
);
}
}
const results = await Promise.all(checks);
const healthy = results.filter(r => r).length;
const total = results.length;
return {
healthy,
total,
healthPercentage: (healthy / total) * 100,
};
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface TradeResult {
success: boolean;
signature?: string;
slot?: number;
duration: number;
attempts: number;
error?: string;
}
interface BatchTradeResult {
total: number;
successful: number;
failed: number;
signatures: string[];
}
interface MarketData {
slot: number;
timestamp: number;
data: Buffer;
lamports: number;
}
interface HealthStatus {
healthy: number;
total: number;
healthPercentage: number;
}
// Usage example
async function exampleHFT() {
const trader = new HighFrequencyTrader(
{
primary: 'https://rpc.solana.com',
fallbacks: [
'https://api.mainnet-beta.solana.com',
'https://solana-api.projectserum.com',
],
wsUrl: 'wss://rpc.solana.com',
maxConnectionsPerEndpoint: 5,
},
{
maxSlippage: 0.01,
priorityFee: 10000,
confirmationTimeout: 30000,
retryAttempts: 3,
}
);
// Health check
const health = await trader.healthCheck();
console.log(`Connection health: ${health.healthPercentage.toFixed(1)}%`);
// Subscribe to market data
const marketAccount = new PublicKey('MarketAccountAddressHere');
trader.subscribeToMarketData(marketAccount, (data) => {
console.log(`Market update at slot ${data.slot}`);
// Analyze and potentially execute trade
// if (shouldTrade(data)) {
// trader.executeTrade(tradeInstruction, payer);
// }
});
}Performance Considerations
- Connection Pooling: Multiple connections to same endpoint for parallel requests
- Skip Preflight: Disable preflight checks for faster submission
- Priority Fees: Use priority fees to increase confirmation chances
- Slot Tracking: Time transactions optimally using slot updates
- Failover: Automatically switch to backup RPCs on failure
Cost Optimization
| Strategy | Credits Saved | Trade-off |
|---|---|---|
| Connection pooling | 0 (same calls) | More infrastructure |
| Skip preflight | 10 per tx | Higher failure risk |
| Batch transactions | 50-80% | Atomic execution |
| WebSocket for data | 90% vs polling | Real-time only |
Smart Caching Strategy
Implement intelligent caching to reduce costs and improve performance.
Use Case
Build a caching layer that optimally stores Solana data with appropriate invalidation strategies.
Architecture
graph TB
A[Request] --> B{Cache Hit?}
B -->|Yes| C[Return Cached]
B -->|No| D[Fetch from RPC]
D --> E[Store in Cache]
E --> C
F[WebSocket Update] --> G[Invalidate Cache]
G --> H[Update Cache]
I[TTL Expiration] --> G
J[Commitment Level] --> K[Cache Strategy]Complete Implementation
import { Connection, PublicKey, AccountInfo, Commitment } from '@solana/web3.js';
import Redis from 'ioredis';
interface CacheConfig {
ttl: {
accounts: number;
blocks: number;
transactions: number;
programAccounts: number;
};
redis?: {
host: string;
port: number;
};
}
class SmartCache {
private connection: Connection;
private wsConnection: Connection;
private memoryCache: Map<string, CacheEntry>;
private redis?: Redis;
private config: CacheConfig;
private subscriptions: Map<string, number>;
constructor(rpcUrl: string, wsUrl: string, config: CacheConfig) {
this.connection = new Connection(rpcUrl, 'confirmed');
this.wsConnection = new Connection(wsUrl, {
commitment: 'confirmed',
wsEndpoint: wsUrl,
});
this.memoryCache = new Map();
this.config = config;
this.subscriptions = new Map();
// Initialize Redis if configured
if (config.redis) {
this.redis = new Redis({
host: config.redis.host,
port: config.redis.port,
});
}
// Start cache cleanup
this.startCleanup();
}
/**
* Get account with caching
*/
async getAccountInfo(
pubkey: PublicKey,
commitment: Commitment = 'confirmed'
): Promise<AccountInfo<Buffer> | null> {
const key = `account:${pubkey.toString()}:${commitment}`;
// Check cache
const cached = await this.get<AccountInfo<Buffer>>(key);
if (cached) {
console.log(`Cache hit: ${key}`);
return cached;
}
console.log(`Cache miss: ${key}`);
// Fetch from RPC
const accountInfo = await this.connection.getAccountInfo(pubkey, commitment);
// Cache result
await this.set(key, accountInfo, this.config.ttl.accounts);
// Subscribe to updates if not already subscribed
if (commitment === 'confirmed' && !this.subscriptions.has(pubkey.toString())) {
this.subscribeToAccount(pubkey);
}
return accountInfo;
}
/**
* Get multiple accounts with batch caching
*/
async getMultipleAccountsInfo(
pubkeys: PublicKey[],
commitment: Commitment = 'confirmed'
): Promise<(AccountInfo<Buffer> | null)[]> {
const keys = pubkeys.map(pk => `account:${pk.toString()}:${commitment}`);
// Check cache for all keys
const cacheResults = await Promise.all(
keys.map(key => this.get<AccountInfo<Buffer>>(key))
);
// Identify missing accounts
const missingIndices: number[] = [];
const missingPubkeys: PublicKey[] = [];
cacheResults.forEach((result, i) => {
if (result === null) {
missingIndices.push(i);
missingPubkeys.push(pubkeys[i]);
}
});
// Fetch missing accounts
if (missingPubkeys.length > 0) {
console.log(`Cache miss for ${missingPubkeys.length} accounts`);
const fetched = await this.connection.getMultipleAccountsInfo(
missingPubkeys,
commitment
);
// Cache fetched accounts
for (let i = 0; i < fetched.length; i++) {
const originalIndex = missingIndices[i];
cacheResults[originalIndex] = fetched[i];
await this.set(
keys[originalIndex],
fetched[i],
this.config.ttl.accounts
);
}
} else {
console.log(`Cache hit for all ${pubkeys.length} accounts`);
}
return cacheResults;
}
/**
* Get transaction with caching
*/
async getTransaction(
signature: string,
commitment: Commitment = 'confirmed'
): Promise<any> {
const key = `transaction:${signature}:${commitment}`;
// Transactions are immutable once confirmed, cache indefinitely
const cached = await this.get(key);
if (cached) {
console.log(`Cache hit: ${key}`);
return cached;
}
console.log(`Cache miss: ${key}`);
const transaction = await this.connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
commitment,
});
if (transaction) {
// Cache indefinitely for finalized transactions
const ttl = commitment === 'finalized'
? this.config.ttl.transactions * 10
: this.config.ttl.transactions;
await this.set(key, transaction, ttl);
}
return transaction;
}
/**
* Get program accounts with smart caching
*/
async getProgramAccounts(
programId: PublicKey,
filters?: any[]
): Promise<any[]> {
// Create cache key from filters
const filterKey = filters ? JSON.stringify(filters) : 'all';
const key = `program:${programId.toString()}:${filterKey}`;
const cached = await this.get<any[]>(key);
if (cached) {
console.log(`Cache hit: ${key}`);
return cached;
}
console.log(`Cache miss: ${key}`);
const accounts = await this.connection.getProgramAccounts(programId, {
filters,
});
// Shorter TTL for program accounts as they change frequently
await this.set(key, accounts, this.config.ttl.programAccounts);
return accounts;
}
/**
* Subscribe to account updates and invalidate cache
*/
private subscribeToAccount(pubkey: PublicKey): void {
const subscriptionId = this.wsConnection.onAccountChange(
pubkey,
async (accountInfo, context) => {
console.log(`Account updated: ${pubkey.toString()} at slot ${context.slot}`);
// Invalidate all commitment levels for this account
const commitments: Commitment[] = ['processed', 'confirmed', 'finalized'];
for (const commitment of commitments) {
const key = `account:${pubkey.toString()}:${commitment}`;
await this.delete(key);
// Update cache with new data for confirmed
if (commitment === 'confirmed') {
await this.set(key, accountInfo, this.config.ttl.accounts);
}
}
},
'confirmed'
);
this.subscriptions.set(pubkey.toString(), subscriptionId);
}
/**
* Get from cache (memory first, then Redis)
*/
private async get<T>(key: string): Promise<T | null> {
// Check memory cache first
const memCached = this.memoryCache.get(key);
if (memCached && memCached.expiresAt > Date.now()) {
return memCached.value as T;
}
// Check Redis if available
if (this.redis) {
try {
const cached = await this.redis.get(key);
if (cached) {
const parsed = JSON.parse(cached);
// Update memory cache
this.memoryCache.set(key, {
value: parsed,
expiresAt: Date.now() + this.config.ttl.accounts * 1000,
});
return parsed as T;
}
} catch (error) {
console.error('Redis get error:', error);
}
}
return null;
}
/**
* Set in cache (memory and Redis)
*/
private async set(key: string, value: any, ttlSeconds: number): Promise<void> {
const expiresAt = Date.now() + ttlSeconds * 1000;
// Set in memory cache
this.memoryCache.set(key, { value, expiresAt });
// Set in Redis if available
if (this.redis) {
try {
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
} catch (error) {
console.error('Redis set error:', error);
}
}
}
/**
* Delete from cache
*/
private async delete(key: string): Promise<void> {
this.memoryCache.delete(key);
if (this.redis) {
try {
await this.redis.del(key);
} catch (error) {
console.error('Redis delete error:', error);
}
}
}
/**
* Start periodic cache cleanup
*/
private startCleanup(): void {
setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.memoryCache.entries()) {
if (entry.expiresAt <= now) {
this.memoryCache.delete(key);
}
}
console.log(`Cache cleanup: ${this.memoryCache.size} entries remaining`);
}, 60000); // Every minute
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
let totalEntries = this.memoryCache.size;
let expiredEntries = 0;
const now = Date.now();
for (const [_, entry] of this.memoryCache.entries()) {
if (entry.expiresAt <= now) {
expiredEntries++;
}
}
return {
totalEntries,
activeEntries: totalEntries - expiredEntries,
expiredEntries,
subscriptions: this.subscriptions.size,
};
}
/**
* Clear all cache
*/
async clearAll(): Promise<void> {
this.memoryCache.clear();
if (this.redis) {
await this.redis.flushdb();
}
console.log('Cache cleared');
}
}
interface CacheEntry {
value: any;
expiresAt: number;
}
interface CacheStats {
totalEntries: number;
activeEntries: number;
expiredEntries: number;
subscriptions: number;
}
// Usage example
async function exampleCaching() {
const cache = new SmartCache(
'https://rpc.solana.com',
'wss://rpc.solana.com',
{
ttl: {
accounts: 60, // 1 minute
blocks: 600, // 10 minutes
transactions: 3600, // 1 hour
programAccounts: 30, // 30 seconds
},
redis: {
host: 'localhost',
port: 6379,
},
}
);
const pubkey = new PublicKey('AccountAddressHere');
// First call - cache miss
let start = Date.now();
let account1 = await cache.getAccountInfo(pubkey);
console.log(`First call: ${Date.now() - start}ms`);
// Second call - cache hit
start = Date.now();
let account2 = await cache.getAccountInfo(pubkey);
console.log(`Second call: ${Date.now() - start}ms`);
// Get multiple accounts
const pubkeys = [
new PublicKey('Account1'),
new PublicKey('Account2'),
new PublicKey('Account3'),
];
const accounts = await cache.getMultipleAccountsInfo(pubkeys);
console.log(`Fetched ${accounts.length} accounts`);
// Cache stats
const stats = cache.getStats();
console.log('Cache stats:', stats);
}Cache Strategy by Data Type
| Data Type | TTL | Invalidation | Commitment |
|---|---|---|---|
| Finalized transactions | 1 hour+ | Never | finalized |
| Confirmed transactions | 5 minutes | On reorg | confirmed |
| Account data (static) | 10 minutes | On update | confirmed |
| Account data (dynamic) | 30 seconds | WebSocket | confirmed |
| Program accounts | 30 seconds | Manual | confirmed |
| Blocks | 10 minutes | Never | finalized |
| Token prices | 1 minute | Time-based | N/A |
Performance Gains
| Scenario | Without Cache | With Cache | Improvement |
|---|---|---|---|
| Get account | 150ms | 2ms | 75x faster |
| Get 100 accounts | 2000ms | 50ms | 40x faster |
| Get transaction | 200ms | 2ms | 100x faster |
| Program accounts | 500ms | 5ms | 100x faster |
Multi-Region Failover
Implement robust failover logic for global reliability.
Use Case
Ensure your application remains operational even when RPC endpoints fail or experience degraded performance.
Architecture
graph TB
A[Request] --> B[Primary RPC]
B -->|Timeout/Error| C[Health Check]
C -->|Unhealthy| D[Switch to Fallback]
D --> E[Fallback RPC 1]
E -->|Timeout/Error| F[Fallback RPC 2]
G[Monitor] --> H[Track Latency]
H --> I[Update Rankings]
I --> J[Select Best Endpoint]Complete Implementation
import { Connection, Commitment } from '@solana/web3.js';
import axios from 'axios';
interface EndpointConfig {
url: string;
region: string;
priority: number;
}
interface EndpointHealth {
url: string;
healthy: boolean;
latency: number;
lastCheck: number;
successRate: number;
errorCount: number;
}
class MultiRegionFailover {
private endpoints: EndpointConfig[];
private health: Map<string, EndpointHealth>;
private currentEndpoint: string;
private connections: Map<string, Connection>;
constructor(endpoints: EndpointConfig[]) {
this.endpoints = endpoints.sort((a, b) => a.priority - b.priority);
this.health = new Map();
this.connections = new Map();
this.currentEndpoint = endpoints[0].url;
// Initialize health tracking
for (const endpoint of endpoints) {
this.health.set(endpoint.url, {
url: endpoint.url,
healthy: true,
latency: 0,
lastCheck: 0,
successRate: 100,
errorCount: 0,
});
this.connections.set(
endpoint.url,
new Connection(endpoint.url, 'confirmed')
);
}
// Start health monitoring
this.startHealthMonitoring();
}
/**
* Get current connection with automatic failover
*/
getConnection(): Connection {
const connection = this.connections.get(this.currentEndpoint);
if (!connection) {
throw new Error('No healthy endpoints available');
}
return connection;
}
/**
* Execute request with automatic failover
*/
async executeWithFailover<T>(
operation: (connection: Connection) => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: Error | null = null;
let attemptedEndpoints: string[] = [];
for (let attempt = 0; attempt < maxRetries; attempt++) {
const endpoint = this.selectBestEndpoint(attemptedEndpoints);
if (!endpoint) {
throw new Error('No healthy endpoints available');
}
attemptedEndpoints.push(endpoint);
const connection = this.connections.get(endpoint)!;
const health = this.health.get(endpoint)!;
try {
const start = Date.now();
const result = await this.withTimeout(
operation(connection),
10000 // 10 second timeout
);
// Update health metrics
health.latency = Date.now() - start;
health.successRate = Math.min(100, health.successRate + 1);
health.errorCount = Math.max(0, health.errorCount - 1);
return result;
} catch (error) {
lastError = error as Error;
console.error(`Request failed on ${endpoint}:`, error);
// Update health metrics
health.errorCount++;
health.successRate = Math.max(0, health.successRate - 5);
// Mark as unhealthy if too many errors
if (health.errorCount >= 3) {
health.healthy = false;
console.warn(`Marking ${endpoint} as unhealthy`);
}
// Try next endpoint
if (attempt < maxRetries - 1) {
await this.delay(Math.pow(2, attempt) * 100);
}
}
}
throw new Error(
`All endpoints failed after ${maxRetries} attempts: ${lastError?.message}`
);
}
/**
* Select best endpoint based on health and priority
*/
private selectBestEndpoint(exclude: string[] = []): string | null {
const candidates = this.endpoints.filter(
ep => !exclude.includes(ep.url)
);
// Filter to healthy endpoints
const healthy = candidates.filter(ep => {
const health = this.health.get(ep.url);
return health?.healthy && health.successRate > 50;
});
if (healthy.length === 0) {
// No healthy endpoints, try any not excluded
if (candidates.length > 0) {
return candidates[0].url;
}
return null;
}
// Sort by latency and priority
healthy.sort((a, b) => {
const healthA = this.health.get(a.url)!;
const healthB = this.health.get(b.url)!;
// Prioritize by priority first
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
// Then by latency
return healthA.latency - healthB.latency;
});
const selected = healthy[0].url;
this.currentEndpoint = selected;
return selected;
}
/**
* Start periodic health monitoring
*/
private startHealthMonitoring(): void {
const CHECK_INTERVAL = 30000; // 30 seconds
setInterval(async () => {
for (const endpoint of this.endpoints) {
await this.checkEndpointHealth(endpoint.url);
}
// Log health status
this.logHealthStatus();
}, CHECK_INTERVAL);
// Initial health check
this.endpoints.forEach(ep => this.checkEndpointHealth(ep.url));
}
/**
* Check health of a specific endpoint
*/
private async checkEndpointHealth(url: string): Promise<void> {
const health = this.health.get(url);
if (!health) return;
try {
const connection = this.connections.get(url)!;
const start = Date.now();
// Simple health check - get slot
await this.withTimeout(connection.getSlot(), 5000);
const latency = Date.now() - start;
// Update health
health.healthy = true;
health.latency = latency;
health.lastCheck = Date.now();
health.errorCount = Math.max(0, health.errorCount - 1);
console.log(`Health check passed for ${url}: ${latency}ms`);
} catch (error) {
console.error(`Health check failed for ${url}:`, error);
health.errorCount++;
health.lastCheck = Date.now();
if (health.errorCount >= 3) {
health.healthy = false;
}
}
}
/**
* Log current health status
*/
private logHealthStatus(): void {
console.log('\n=== Endpoint Health Status ===');
for (const endpoint of this.endpoints) {
const health = this.health.get(endpoint.url);
if (!health) continue;
const status = health.healthy ? 'HEALTHY' : 'UNHEALTHY';
const current = endpoint.url === this.currentEndpoint ? '[CURRENT]' : '';
console.log(
`${status} ${current} ${endpoint.region} - ${endpoint.url}` +
`\n Latency: ${health.latency}ms | Success Rate: ${health.successRate.toFixed(1)}% | ` +
`Errors: ${health.errorCount}`
);
}
console.log('=============================\n');
}
/**
* Get health status for all endpoints
*/
getHealthStatus(): EndpointHealth[] {
return Array.from(this.health.values());
}
/**
* Manually mark endpoint as healthy/unhealthy
*/
setEndpointHealth(url: string, healthy: boolean): void {
const health = this.health.get(url);
if (health) {
health.healthy = healthy;
health.errorCount = healthy ? 0 : 5;
}
}
/**
* Add timeout to promise
*/
private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeoutMs)
),
]);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage example
async function exampleFailover() {
const failover = new MultiRegionFailover([
{
url: 'https://rpc.solana.com',
region: 'US-East',
priority: 1,
},
{
url: 'https://api.mainnet-beta.solana.com',
region: 'US-West',
priority: 2,
},
{
url: 'https://solana-api.projectserum.com',
region: 'EU',
priority: 3,
},
]);
// Execute operation with automatic failover
try {
const slot = await failover.executeWithFailover(
async (connection) => {
return await connection.getSlot();
}
);
console.log('Current slot:', slot);
} catch (error) {
console.error('All endpoints failed:', error);
}
// Get health status
const health = failover.getHealthStatus();
console.log('Endpoint health:', health);
// Use in application
const connection = failover.getConnection();
const balance = await failover.executeWithFailover(
async (conn) => await conn.getBalance(new PublicKey('WalletAddressHere'))
);
console.log('Balance:', balance);
}Failover Strategies
| Strategy | Use Case | Implementation |
|---|---|---|
| Round-robin | Equal priority | Rotate through endpoints |
| Latency-based | Performance critical | Select lowest latency |
| Priority-based | Cost optimization | Use cheapest first |
| Geographic | Regional apps | Select closest region |
| Hybrid | Production | Priority + latency + health |
Monitoring and Alerts
interface AlertConfig {
webhook?: string;
email?: string;
slackChannel?: string;
}
class FailoverAlerts {
private config: AlertConfig;
constructor(config: AlertConfig) {
this.config = config;
}
async sendAlert(alert: Alert): Promise<void> {
console.error('ALERT:', alert.message);
if (this.config.webhook) {
try {
await axios.post(this.config.webhook, {
type: alert.type,
message: alert.message,
timestamp: alert.timestamp,
endpoint: alert.endpoint,
});
} catch (error) {
console.error('Failed to send webhook alert:', error);
}
}
// Add email, Slack, etc.
}
}
interface Alert {
type: 'endpoint_down' | 'all_endpoints_down' | 'high_latency' | 'degraded_performance';
message: string;
timestamp: number;
endpoint?: string;
}Performance Considerations
- Health Check Frequency: Balance between responsiveness and overhead (30-60 seconds)
- Timeout Values: Set appropriate timeouts (5-10 seconds for health checks)
- Connection Pooling: Maintain persistent connections to all endpoints
- Circuit Breaker: Temporarily disable failing endpoints
Testing Approach
All patterns should include comprehensive tests:
// Example test structure
describe('PaymentProcessor', () => {
let processor: PaymentProcessor;
beforeEach(() => {
processor = new PaymentProcessor(TEST_RPC, TEST_WS);
});
it('should process valid payment', async () => {
// Test implementation
});
it('should handle timeout', async () => {
// Test timeout scenario
});
it('should detect reorg', async () => {
// Test reorg detection
});
});Best Practices Summary
- Error Handling: Always implement retry logic with exponential backoff
- Rate Limiting: Respect RPC rate limits with delays between batches
- Connection Management: Use connection pooling for high-frequency operations
- Caching: Cache immutable data (finalized transactions, blocks)
- Monitoring: Track success rates, latencies, and errors
- Failover: Implement multi-region failover for production
- Cost Optimization: Batch requests, use WebSocket, cache aggressively
- Testing: Test all edge cases, timeouts, and failure scenarios