@btc-stamps/tx-builder
Version:
Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection
371 lines (327 loc) • 10.2 kB
text/typescript
/**
* Mock ElectrumX Fee Rate Provider
* Provides mock fee rates for testing and development
* NOTE: This is a mock implementation - use ElectrumXFeeEstimator for production
*/
import type { FeeRate } from '../interfaces/fee.interface.ts';
import process from 'node:process';
import { getElectrumXEndpoints } from '../config/electrumx-config.ts';
import { clearTimeoutCompat, setTimeoutCompat, type TimerId } from '../utils/timer-utils.ts';
export interface ElectrumXConfig {
servers: Array<{
host: string;
port: number;
ssl?: boolean;
timeout?: number;
}>;
fallbackFeeRate?: number;
cacheTimeout?: number; // seconds
maxRetries?: number;
}
export interface ElectrumXFeeResponse {
1: number; // Next block
3: number; // 3 blocks
6: number; // 6 blocks
12: number; // 12 blocks
25: number; // 25 blocks
}
/**
* Mock ElectrumX fee estimation provider for testing and development
*/
export class MockElectrumXFeeProvider {
private config: Required<ElectrumXConfig>;
private cache: Map<string, { data: FeeRate; timestamp: number }> = new Map();
constructor(config: ElectrumXConfig) {
this.config = {
servers: config.servers,
fallbackFeeRate: config.fallbackFeeRate ?? 10,
cacheTimeout: config.cacheTimeout ?? 60, // 1 minute
maxRetries: config.maxRetries ?? 3,
};
}
/**
* Get fee rates from ElectrumX servers
*/
getFeeRates(): Promise<FeeRate> {
const cacheKey = 'electrumx_fees';
const cached = this.cache.get(cacheKey);
// Return cached data if still valid
if (cached && this.isCacheValid(cached.timestamp)) {
return Promise.resolve(cached.data);
}
return this.fetchFeeRatesFromServers()
.then((feeData) => {
const feeRates = this.processFeeResponse(feeData);
// Cache the result
this.cache.set(cacheKey, {
data: feeRates,
timestamp: Date.now(),
});
return feeRates;
})
.catch((error) => {
console.warn('ElectrumX fee estimation failed, using fallback:', error);
return this.getFallbackFeeRates();
});
}
/**
* Fetch fee rates from multiple ElectrumX servers
*/
private fetchFeeRatesFromServers(): Promise<ElectrumXFeeResponse> {
const errors: string[] = [];
return new Promise((resolve, reject) => {
for (const server of this.config.servers) {
try {
const response = this.queryElectrumXServer(server);
response.then((feeResponse) => {
if (this.isValidFeeResponse(feeResponse)) {
resolve(feeResponse);
}
}).catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(`${server.host}:${server.port} - ${errorMessage}`);
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(`${server.host}:${server.port} - ${errorMessage}`);
}
}
// If no valid response is found after trying all servers
setTimeout(() => {
reject(new Error(`All ElectrumX servers failed: ${errors.join(', ')}`));
}, 5000);
});
}
/**
* Query a single ElectrumX server for fee estimation
*/
private queryElectrumXServer(server: {
host: string;
port: number;
ssl?: boolean;
timeout?: number;
}): Promise<ElectrumXFeeResponse> {
const _protocol = server.ssl ? 'wss' : 'ws';
console.debug(`Attempting connection using protocol: ${_protocol}`); // Use the unused variable
const timeout = server.timeout ?? 5000;
return new Promise((resolve, reject) => {
let ws: WebSocket | undefined;
let timeoutId: TimerId | undefined;
const cleanup = () => {
if (timeoutId) clearTimeoutCompat(timeoutId);
if (ws) {
// WebSocket doesn't have removeAllListeners, use proper cleanup
if (
ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING
) {
ws.close();
}
}
};
try {
// In a real implementation, you'd use a proper WebSocket or TCP connection
// For now, simulate the ElectrumX response structure
timeoutId = setTimeoutCompat(() => {
cleanup();
reject(
new Error(`Timeout connecting to ${server.host}:${server.port}`),
);
}, timeout);
// Mock ElectrumX blockchain.estimatefee response
// In real implementation: ws = new WebSocket(`${protocol}://${server.host}:${server.port}`);
// Simulate network delay
setTimeout(
() => {
cleanup();
// Mock response based on typical ElectrumX fee estimates
const mockResponse: ElectrumXFeeResponse = {
1: this.generateRealisticFeeRate('urgent'),
3: this.generateRealisticFeeRate('high'),
6: this.generateRealisticFeeRate('medium'),
12: this.generateRealisticFeeRate('low'),
25: this.generateRealisticFeeRate('economy'),
};
resolve(mockResponse);
},
100 + Math.random() * 200,
); // 100-300ms delay
} catch (error) {
cleanup();
reject(error);
}
});
}
/**
* Generate realistic fee rates (mock implementation)
* In production, this would be actual ElectrumX responses
*/
private generateRealisticFeeRate(priority: string): number {
// Base on current network conditions (mock)
const baseRates = {
economy: 2,
low: 5,
medium: 15,
high: 30,
urgent: 50,
};
const baseRate = baseRates[priority as keyof typeof baseRates] || 10;
// Add some randomness to simulate real network conditions
const variance = baseRate * 0.2; // ±20% variance
const adjustment = (Math.random() - 0.5) * 2 * variance;
return Math.max(1, Math.round(baseRate + adjustment));
}
/**
* Process ElectrumX fee response into standard format
*/
private processFeeResponse(response: ElectrumXFeeResponse): FeeRate {
return {
urgent: response[1] || 50, // Next block
high: response[3] || 30, // 3 blocks
medium: response[6] || 15, // 6 blocks
low: response[25] || 5, // 25 blocks
};
}
/**
* Validate ElectrumX fee response
*/
private isValidFeeResponse(response: any): response is ElectrumXFeeResponse {
return (
response &&
typeof response === 'object' &&
typeof response[1] === 'number' &&
typeof response[3] === 'number' &&
typeof response[6] === 'number' &&
response[1] > 0 &&
response[3] > 0 &&
response[6] > 0
);
}
/**
* Get fallback fee rates when ElectrumX is unavailable
*/
private getFallbackFeeRates(): FeeRate {
const base = this.config.fallbackFeeRate;
return {
low: Math.max(1, Math.round(base * 0.5)),
medium: base,
high: Math.round(base * 2),
urgent: Math.round(base * 3),
};
}
/**
* Check if cached data is still valid
*/
private isCacheValid(timestamp: number): boolean {
const age = (Date.now() - timestamp) / 1000; // Age in seconds
return age < this.config.cacheTimeout;
}
/**
* Mock transaction getter for testing
*/
getTransaction(txid: string): Promise<
{
txid: string;
size: number;
vsize: number;
fee?: number;
} | null
> {
// Return a mock transaction for testing
if (txid === 'not-found') {
return Promise.resolve(null);
}
return Promise.resolve({
txid,
size: 250,
vsize: 180,
fee: 2000, // 2000 sats
});
}
/**
* Clear cached fee data
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get current cache stats
*/
getCacheStats(): {
size: number;
entries: Array<{ key: string; age: number }>;
} {
const entries = Array.from(this.cache.entries()).map(([key, value]) => ({
key,
age: (Date.now() - value.timestamp) / 1000,
}));
return {
size: this.cache.size,
entries,
};
}
/**
* Update server configuration
*/
updateConfig(newConfig: Partial<ElectrumXConfig>): void {
this.config = { ...this.config, ...newConfig };
// Clear cache when config changes
this.clearCache();
}
/**
* Test connection to all configured servers
*/
async testConnections(): Promise<
Array<
{ server: string; success: boolean; latency?: number; error?: string }
>
> {
const results = [];
for (const server of this.config.servers) {
const startTime = Date.now();
try {
await this.queryElectrumXServer(server);
const latency = Date.now() - startTime;
results.push({
server: `${server.host}:${server.port}`,
success: true,
latency,
});
} catch (error) {
results.push({
server: `${server.host}:${server.port}`,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return results;
}
}
/**
* Create mock ElectrumX fee provider with configuration from environment variables
*/
export function createMockElectrumXFeeProvider(
customServers?: ElectrumXConfig['servers'],
network: string = 'mainnet',
): MockElectrumXFeeProvider {
let servers: ElectrumXConfig['servers'];
if (customServers) {
servers = customServers;
} else {
// Get servers from centralized configuration
const endpoints = getElectrumXEndpoints(network);
servers = endpoints.map((endpoint) => ({
host: endpoint.host,
port: endpoint.port,
ssl: endpoint.protocol === 'ssl' || endpoint.protocol === 'wss',
timeout: endpoint.timeout,
}));
}
return new MockElectrumXFeeProvider({
servers,
fallbackFeeRate: parseInt(process.env.ELECTRUMX_FALLBACK_FEE_RATE || '10'),
cacheTimeout: parseInt(process.env.ELECTRUMX_FEE_CACHE_TIMEOUT || '60'),
maxRetries: parseInt(process.env.ELECTRUMX_MAX_RETRIES || '3'),
});
}