@btc-stamps/tx-builder
Version:
Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection
520 lines (459 loc) • 13.4 kB
text/typescript
/**
* ElectrumX Fee Estimator
* Advanced fee estimation with caching, validation, and fallback mechanisms
*/
import type { ElectrumXConnectionPool } from './electrumx-connection-pool.ts';
import type { ElectrumXProvider } from './electrumx-provider.ts';
export interface FeeEstimate {
priority: 'economy' | 'low' | 'medium' | 'high' | 'urgent';
confirmationTarget: number;
feeRate: number; // sat/vB
estimatedTime: string; // Human readable
confidence: 'low' | 'medium' | 'high';
source: 'electrumx' | 'fallback' | 'cached';
timestamp: number;
}
export interface FeeEstimationOptions {
includeFallback: boolean;
cacheTimeout: number; // seconds
validationThreshold: number; // maximum reasonable fee rate
minFeeRate: number; // minimum fee rate
maxConfirmationTarget: number;
}
interface CachedEstimate {
estimate: FeeEstimate;
timestamp: number;
}
/**
* Advanced fee estimation with multiple strategies and validation
*/
export class ElectrumXFeeEstimator {
private cache = new Map<string, CachedEstimate>();
public readonly provider: ElectrumXProvider | ElectrumXConnectionPool;
private options: Required<FeeEstimationOptions>;
constructor(
provider: ElectrumXProvider | ElectrumXConnectionPool,
options?: Partial<FeeEstimationOptions>,
) {
this.provider = provider;
this.options = {
includeFallback: true,
cacheTimeout: 60, // 1 minute
validationThreshold: 1000, // 1000 sat/vB max
minFeeRate: 1, // 1 sat/vB min
maxConfirmationTarget: 144, // ~24 hours
...options,
};
}
/**
* Get comprehensive fee estimates for all priority levels
*/
async getAllFeeEstimates(): Promise<FeeEstimate[]> {
const priorities: Array<{
priority: FeeEstimate['priority'];
target: number;
time: string;
}> = [
{ priority: 'urgent', target: 1, time: '~10 minutes' },
{ priority: 'high', target: 3, time: '~30 minutes' },
{ priority: 'medium', target: 6, time: '~1 hour' },
{ priority: 'low', target: 12, time: '~2 hours' },
{ priority: 'economy', target: 25, time: '~4 hours' },
];
const estimates = await Promise.allSettled(
priorities.map(async ({ priority, target, time }) => {
try {
return await this.getFeeEstimate(priority, target, time);
} catch (error) {
console.warn(`Failed to get ${priority} fee estimate:`, error);
return this.getFallbackEstimate(priority, target, time);
}
}),
);
return estimates
.filter(
(result): result is PromiseFulfilledResult<FeeEstimate> => result.status === 'fulfilled',
)
.map((result) => result.value);
}
/**
* Get fee estimate for specific priority
*/
async getFeeEstimate(
priority: FeeEstimate['priority'],
confirmationTarget?: number,
estimatedTime?: string,
): Promise<FeeEstimate> {
const target = confirmationTarget || this.getDefaultTarget(priority);
const time = estimatedTime || this.getEstimatedTime(target);
const cacheKey = `${priority}-${target}`;
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached && this.isCacheValid(cached.timestamp)) {
return { ...cached.estimate, source: 'cached' };
}
try {
// Get estimate from ElectrumX
const rawFeeRate = await this.provider.getFeeRate(
this.mapPriorityToProviderPriority(priority),
);
// Validate and process the fee rate
const feeRate = this.validateAndProcessFeeRate(rawFeeRate, priority);
const confidence = this.calculateConfidence(feeRate, target);
const estimate: FeeEstimate = {
priority,
confirmationTarget: target,
feeRate,
estimatedTime: time,
confidence,
source: 'electrumx',
timestamp: Date.now(),
};
// Cache the estimate
this.cache.set(cacheKey, {
estimate,
timestamp: Date.now(),
});
return estimate;
} catch (error) {
console.warn(`ElectrumX fee estimation failed for ${priority}:`, error);
if (this.options.includeFallback) {
return this.getFallbackEstimate(priority, target, time);
}
throw error;
}
}
/**
* Get optimal fee for transaction size
*/
async getOptimalFee(
transactionSizeBytes: number,
priority: FeeEstimate['priority'] = 'medium',
): Promise<{
totalFee: number;
feeRate: number;
estimate: FeeEstimate;
}> {
const estimate = await this.getFeeEstimate(priority);
const totalFee = Math.ceil(transactionSizeBytes * estimate.feeRate);
return {
totalFee,
feeRate: estimate.feeRate,
estimate,
};
}
/**
* Get fee estimates from multiple confirmation targets
*/
async getFeeRangeEstimate(
minTarget: number = 1,
maxTarget: number = 25,
): Promise<{
estimates: Array<{
target: number;
feeRate: number;
estimatedTime: string;
}>;
recommended: FeeEstimate;
}> {
const targets = this.generateTargetRange(minTarget, maxTarget);
const estimates = [];
for (const target of targets) {
try {
const priority = this.targetToPriority(target);
const time = this.getEstimatedTime(target);
const estimate = await this.getFeeEstimate(priority, target, time);
estimates.push({
target,
feeRate: estimate.feeRate,
estimatedTime: time,
});
} catch (error) {
console.warn(`Failed to get estimate for target ${target}:`, error);
}
}
// Sort by fee rate and find recommended middle ground
estimates.sort((a, b) => b.feeRate - a.feeRate);
const medianIndex = Math.floor(estimates.length / 2);
const medianEstimate = estimates[medianIndex];
const recommended = await this.getFeeEstimate(
'medium',
medianEstimate?.target || 6,
medianEstimate?.estimatedTime || '~1 hour',
);
return { estimates, recommended };
}
/**
* Validate fee rate and apply corrections
*/
private validateAndProcessFeeRate(
rawFeeRate: number,
priority: FeeEstimate['priority'],
): number {
let feeRate = rawFeeRate;
// Handle negative or zero fee rates
if (feeRate <= 0) {
feeRate = this.getDefaultFeeRate(priority);
}
// Apply minimum fee rate
feeRate = Math.max(feeRate, this.options.minFeeRate);
// Apply maximum fee rate (protection against unreasonable rates)
if (feeRate > this.options.validationThreshold) {
console.warn(
`Fee rate ${feeRate} exceeds threshold, capping at ${this.options.validationThreshold}`,
);
feeRate = this.options.validationThreshold;
}
// Apply priority-based adjustments
feeRate = this.applyPriorityAdjustment(feeRate, priority);
return Math.max(1, Math.round(feeRate));
}
/**
* Apply priority-based fee rate adjustments
*/
private applyPriorityAdjustment(
baseFeeRate: number,
priority: FeeEstimate['priority'],
): number {
const multipliers = {
economy: 0.5,
low: 0.8,
medium: 1.0,
high: 1.5,
urgent: 2.0,
};
return baseFeeRate * multipliers[priority];
}
/**
* Calculate confidence level based on fee rate and target
*/
private calculateConfidence(
feeRate: number,
confirmationTarget: number,
): FeeEstimate['confidence'] {
// Simple heuristic: higher fee rates and lower targets = higher confidence
const feeScore = Math.min(feeRate / 50, 1); // Normalize around 50 sat/vB
const targetScore = Math.max(0, 1 - (confirmationTarget - 1) / 24); // Lower target = higher score
const combinedScore = (feeScore + targetScore) / 2;
if (combinedScore > 0.7) return 'high';
if (combinedScore > 0.4) return 'medium';
return 'low';
}
/**
* Get fallback estimate when ElectrumX is unavailable
*/
private getFallbackEstimate(
priority: FeeEstimate['priority'],
confirmationTarget: number,
estimatedTime: string,
): FeeEstimate {
const fallbackRates = {
economy: 2,
low: 5,
medium: 15,
high: 30,
urgent: 50,
};
return {
priority,
confirmationTarget,
feeRate: fallbackRates[priority],
estimatedTime,
confidence: 'low',
source: 'fallback',
timestamp: Date.now(),
};
}
/**
* Map priority to provider priority format
*/
private mapPriorityToProviderPriority(
priority: FeeEstimate['priority'],
): 'low' | 'medium' | 'high' {
switch (priority) {
case 'economy':
case 'low':
return 'low';
case 'high':
case 'urgent':
return 'high';
default:
return 'medium';
}
}
/**
* Get default confirmation target for priority
*/
private getDefaultTarget(priority: FeeEstimate['priority']): number {
const targets = {
urgent: 1,
high: 3,
medium: 6,
low: 12,
economy: 25,
};
return targets[priority];
}
/**
* Get default fee rate for priority (fallback)
*/
private getDefaultFeeRate(priority: FeeEstimate['priority']): number {
const rates = {
urgent: 50,
high: 30,
medium: 15,
low: 5,
economy: 2,
};
return rates[priority];
}
/**
* Convert confirmation target to priority
*/
private targetToPriority(target: number): FeeEstimate['priority'] {
if (target <= 1) return 'urgent';
if (target <= 3) return 'high';
if (target <= 6) return 'medium';
if (target <= 12) return 'low';
return 'economy';
}
/**
* Get estimated confirmation time for target
*/
private getEstimatedTime(confirmationTarget: number): string {
const minutes = confirmationTarget * 10; // ~10 minutes per block
if (minutes < 60) {
return `~${minutes} minutes`;
}
const hours = Math.round(minutes / 60);
return `~${hours} hour${hours === 1 ? '' : 's'}`;
}
/**
* Generate range of confirmation targets
*/
private generateTargetRange(min: number, max: number): number[] {
const targets = [1, 2, 3, 6, 12, 25, 50, 100, 144];
return targets.filter((t) => t >= min && t <= max);
}
/**
* Check if cached estimate is still valid
*/
private isCacheValid(timestamp: number): boolean {
return (Date.now() - timestamp) / 1000 < this.options.cacheTimeout;
}
/**
* Clear expired cache entries
*/
private cleanupCache(): void {
const _now = Date.now();
for (const [key, cached] of this.cache.entries()) {
if (!this.isCacheValid(cached.timestamp)) {
this.cache.delete(key);
}
}
}
/**
* Get cache statistics
*/
getCacheStats(): {
size: number;
hitRate: number;
entries: Array<{
key: string;
age: number;
priority: string;
feeRate: number;
}>;
} {
const entries = Array.from(this.cache.entries()).map(([key, cached]) => ({
key,
age: (Date.now() - cached.timestamp) / 1000,
priority: cached.estimate.priority,
feeRate: cached.estimate.feeRate,
}));
return {
size: this.cache.size,
hitRate: 0, // Would need request tracking to calculate this
entries,
};
}
/**
* Clear all cached estimates
*/
clearCache(): void {
this.cache.clear();
}
/**
* Update estimation options
*/
updateOptions(newOptions: Partial<FeeEstimationOptions>): void {
this.options = { ...this.options, ...newOptions };
this.clearCache(); // Clear cache when options change
}
/**
* Export current fee estimates
*/
async exportFeeEstimates(): Promise<{
timestamp: number;
estimates: FeeEstimate[];
metadata: {
provider: string;
cacheSize: number;
options: FeeEstimationOptions;
};
}> {
const estimates = await this.getAllFeeEstimates();
return {
timestamp: Date.now(),
estimates,
metadata: {
provider: 'electrumx',
cacheSize: this.cache.size,
options: this.options,
},
};
}
/**
* Shutdown fee estimator
*/
shutdown(): void {
this.clearCache();
}
}
/**
* Create ElectrumX fee estimator with default configuration
*/
export function createElectrumXFeeEstimator(
provider: ElectrumXProvider | ElectrumXConnectionPool,
options?: Partial<FeeEstimationOptions>,
): ElectrumXFeeEstimator {
return new ElectrumXFeeEstimator(provider, options);
}
/**
* Create conservative fee estimator (longer cache, higher minimums)
*/
export function createConservativeFeeEstimator(
provider: ElectrumXProvider | ElectrumXConnectionPool,
): ElectrumXFeeEstimator {
return new ElectrumXFeeEstimator(provider, {
includeFallback: true,
cacheTimeout: 300, // 5 minutes
validationThreshold: 500, // Lower max
minFeeRate: 2, // Higher minimum
maxConfirmationTarget: 50,
});
}
/**
* Create aggressive fee estimator (shorter cache, lower minimums)
*/
export function createAggressiveFeeEstimator(
provider: ElectrumXProvider | ElectrumXConnectionPool,
): ElectrumXFeeEstimator {
return new ElectrumXFeeEstimator(provider, {
includeFallback: true,
cacheTimeout: 30, // 30 seconds
validationThreshold: 2000, // Higher max
minFeeRate: 1, // Lower minimum
maxConfirmationTarget: 144,
});
}