@btc-stamps/tx-builder
Version:
Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection
582 lines (513 loc) • 14.5 kB
text/typescript
/**
* ElectrumX Pooled Provider
* High-level provider that uses connection pooling for better performance and reliability
*/
import type { Network } from 'bitcoinjs-lib';
import type {
AddressHistory,
AddressHistoryOptions,
Balance,
ProviderOptions,
Transaction,
UTXO,
} from '../interfaces/provider.interface.ts';
import { getElectrumXEndpoints } from '../config/electrumx-config.ts';
import { BaseProvider } from './base-provider.ts';
import {
type ConnectionPoolOptions,
ElectrumXConnectionPool,
type ElectrumXServer,
} from './electrumx-connection-pool.ts';
export interface ElectrumXPooledOptions extends ProviderOptions {
servers?: ElectrumXServer[];
poolOptions?: Partial<ConnectionPoolOptions>;
}
/**
* ElectrumX Provider with built-in connection pooling and advanced features
*/
export class ElectrumXPooledProvider extends BaseProvider {
private pool: ElectrumXConnectionPool;
constructor(options: ElectrumXPooledOptions) {
super(options);
// Create connection pool
this.pool = new ElectrumXConnectionPool({
network: this.network,
servers: options.servers || this.getServersFromConfig(),
connectionTimeout: this.timeout,
retries: this.retries,
retryDelay: this.retryDelay,
maxRetryDelay: this.maxRetryDelay,
...options.poolOptions,
});
}
/**
* Get UTXOs for a given address
*/
getUTXOs(address: string): Promise<UTXO[]> {
if (!this.isValidAddress(address)) {
throw new Error(`Invalid address: ${address}`);
}
return this.pool.getUTXOs(address);
}
/**
* Get balance for a given address
*/
getBalance(address: string): Promise<Balance> {
if (!this.isValidAddress(address)) {
throw new Error(`Invalid address: ${address}`);
}
return this.pool.getBalance(address);
}
/**
* Get transaction by ID
*/
getTransaction(txid: string): Promise<Transaction> {
if (!this.isValidTxid(txid)) {
throw new Error(`Invalid transaction ID: ${txid}`);
}
return this.pool.getTransaction(txid);
}
/**
* Broadcast a signed transaction
*/
broadcastTransaction(hexTx: string): Promise<string> {
if (!hexTx || typeof hexTx !== 'string' || hexTx.length < 20) {
throw new Error('Invalid transaction hex');
}
return this.pool.broadcastTransaction(hexTx);
}
/**
* Get current fee rate (sat/vB)
*/
getFeeRate(priority: 'low' | 'medium' | 'high' = 'medium'): Promise<number> {
return Promise.resolve(this.pool.getFeeRate(priority));
}
/**
* Get current block height
*/
getBlockHeight(): Promise<number> {
return Promise.resolve(this.pool.getBlockHeight());
}
/**
* Get address transaction history
*/
getAddressHistory(
address: string,
options?: AddressHistoryOptions,
): Promise<AddressHistory[]> {
if (!this.isValidAddress(address)) {
throw new Error(`Invalid address: ${address}`);
}
return this.pool.getAddressHistory(address, options);
}
/**
* Check if provider is connected (checks if any server is healthy)
*/
isConnected(): Promise<boolean> {
const stats = this.pool.getStats();
return Promise.resolve(stats.servers.some((server) => server.healthy));
}
/**
* Get ElectrumX servers from centralized configuration
*/
private getServersFromConfig(): ElectrumXServer[] {
// Get network name for configuration lookup
const networkName = this.getNetworkName();
// Get endpoints from centralized config
const endpoints = getElectrumXEndpoints(networkName);
// Convert ElectrumXEndpoint to ElectrumXServer format
return endpoints.map((endpoint, index) => ({
host: endpoint.host,
port: endpoint.port,
protocol: this.mapProtocol(endpoint.protocol),
weight: this.calculateWeight(endpoint.priority || (index + 1)),
region: this.guessRegion(endpoint.host),
}));
}
/**
* Get network name from Network object
*/
private getNetworkName(): string {
if (this.network && typeof this.network === 'object') {
// Check bech32 prefix to determine network
if ('bech32' in this.network) {
switch (this.network.bech32) {
case 'bc':
return 'mainnet';
case 'tb':
return 'testnet';
case 'bcrt':
return 'regtest';
}
}
}
// Default to mainnet if unable to determine
return 'mainnet';
}
/**
* Map ElectrumX protocol to connection pool protocol
*/
private mapProtocol(
protocol: 'tcp' | 'ssl' | 'ws' | 'wss',
): 'tcp' | 'ssl' | 'ws' | 'wss' {
// For now, they map directly, but this allows for future protocol handling
return protocol;
}
/**
* Calculate weight from priority (lower priority = higher weight)
*/
private calculateWeight(priority: number): number {
// Convert priority (1, 2, 3...) to weight (3, 2, 1...)
return Math.max(1, 4 - priority);
}
/**
* Guess region from hostname (best effort)
*/
private guessRegion(host: string): string {
const hostname = host.toLowerCase();
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
return 'local';
}
// European indicators
if (
hostname.includes('.de') || hostname.includes('.eu') ||
hostname.includes('europe') ||
hostname.includes('aranguren') || hostname.includes('emzy')
) {
return 'eu';
}
// US indicators
if (
hostname.includes('.us') || hostname.includes('qtornado') ||
hostname.includes('fortress')
) {
return 'us';
}
// Asia indicators
if (
hostname.includes('.jp') || hostname.includes('.cn') ||
hostname.includes('.sg') ||
hostname.includes('asia')
) {
return 'asia';
}
// Default to global for well-known global services
return 'global';
}
/**
* Get connection pool statistics
*/
getPoolStats(): {
servers: Array<{
server: string;
healthy: boolean;
activeConnections: number;
totalRequests: number;
successRate: number;
averageResponseTime: number;
consecutiveFailures: number;
healthScore: number;
circuitBreakerState: string;
lastHeartbeat: Date | null;
connectionsInUse: number;
}>;
totalConnections: number;
totalActiveConnections: number;
averageHealthScore: number;
circuitBreakersOpen: number;
} {
return this.pool.getStats();
}
/**
* Get detailed health information
*/
getHealthInfo(): {
healthy: boolean;
healthyServers: number;
totalServers: number;
totalConnections: number;
averageSuccessRate: number;
servers: Array<{
server: string;
healthy: boolean;
successRate: number;
averageResponseTime: number;
activeConnections: number;
}>;
} {
const stats = this.pool.getStats();
const healthyServers = stats.servers.filter((s) => s.healthy);
const averageSuccessRate = stats.servers.length > 0
? stats.servers.reduce((sum, s) => sum + s.successRate, 0) /
stats.servers.length
: 0;
return {
healthy: healthyServers.length > 0,
healthyServers: healthyServers.length,
totalServers: stats.servers.length,
totalConnections: stats.totalConnections,
averageSuccessRate,
servers: stats.servers.map((s) => ({
server: s.server,
healthy: s.healthy,
successRate: s.successRate,
averageResponseTime: s.averageResponseTime,
activeConnections: s.activeConnections,
})),
};
}
/**
* Test connection to all configured servers
*/
async testAllConnections(): Promise<
Array<{
server: string;
success: boolean;
latency?: number;
error?: string;
}>
> {
const stats = this.pool.getStats();
const results = [];
for (const server of stats.servers) {
const startTime = Date.now();
try {
// Test with a simple block height request
await this.pool.getBlockHeight();
const latency = Date.now() - startTime;
results.push({
server: server.server,
success: true,
latency,
});
} catch (error) {
results.push({
server: server.server,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return results;
}
/**
* Get servers by region
*/
getServersByRegion(): Record<string, ElectrumXServer[]> {
const serversList = this.getServersFromConfig();
const regions: Record<string, ElectrumXServer[]> = {};
for (const server of serversList) {
const region = server.region || 'unknown';
if (!regions[region]) {
regions[region] = [];
}
regions[region].push(server);
}
return regions;
}
/**
* Update server configuration
*/
updateServers(servers: ElectrumXServer[]): void {
// This would require extending the pool to support dynamic server updates
console.warn(
`Dynamic server updates not yet implemented for ${servers.length} servers. Restart provider to use new servers.`,
);
}
/**
* Gracefully shutdown the provider
*/
async shutdown(): Promise<void> {
await this.pool.shutdown();
}
}
/**
* Create ElectrumX pooled provider with sensible defaults
*/
export function createElectrumXPooledProvider(
network: Network,
options?: Partial<ElectrumXPooledOptions>,
): ElectrumXPooledProvider {
return new ElectrumXPooledProvider({
network,
timeout: 30000,
retries: 3,
retryDelay: 1000,
maxRetryDelay: 10000,
...options,
});
}
/**
* Create ElectrumX pooled provider with custom server configuration
*/
export function createElectrumXPooledProviderWithServers(
network: Network,
servers: ElectrumXServer[],
options?: Partial<ElectrumXPooledOptions>,
): ElectrumXPooledProvider {
return new ElectrumXPooledProvider({
network,
servers,
timeout: 30000,
retries: 3,
retryDelay: 1000,
maxRetryDelay: 10000,
...options,
});
}
/**
* Create ElectrumX pooled provider optimized for specific use cases
*/
export class ElectrumXPooledProviderBuilder {
private options: Partial<ElectrumXPooledOptions> = {};
constructor(private network: Network) {}
/**
* Set timeout values
*/
withTimeouts(
timeout: number,
retries: number = 3,
retryDelay: number = 1000,
): this {
this.options.timeout = timeout;
this.options.retries = retries;
this.options.retryDelay = retryDelay;
return this;
}
/**
* Configure for high-throughput applications
*/
forHighThroughput(): this {
this.options.poolOptions = {
...this.options.poolOptions,
maxConnectionsPerServer: 5,
loadBalanceStrategy: 'least-connections',
healthCheckInterval: 15000, // 15 seconds
};
return this;
}
/**
* Configure for low-latency applications
*/
forLowLatency(): this {
this.options.poolOptions = {
...this.options.poolOptions,
maxConnectionsPerServer: 2,
loadBalanceStrategy: 'health-based',
healthCheckInterval: 10000, // 10 seconds
connectionTimeout: 5000,
};
return this;
}
/**
* Configure for high reliability
*/
forHighReliability(): this {
this.options.poolOptions = {
...this.options.poolOptions,
failoverThreshold: 2, // Lower threshold
recoveryTimeout: 30000, // Faster recovery
healthCheckInterval: 10000,
};
this.options.retries = 5;
return this;
}
/**
* Add custom servers
*/
withServers(servers: ElectrumXServer[]): this {
this.options.servers = servers;
return this;
}
/**
* Filter servers by region from configured endpoints
*/
withRegion(region: 'us' | 'eu' | 'asia' | 'global' | 'local'): this {
// Get all configured servers
const networkName = this.getNetworkName();
const endpoints = getElectrumXEndpoints(networkName);
// Convert to servers and filter by region
const allServers = endpoints.map((endpoint, index) => ({
host: endpoint.host,
port: endpoint.port,
protocol: endpoint.protocol as 'tcp' | 'ssl' | 'ws' | 'wss',
weight: Math.max(1, 4 - (endpoint.priority || (index + 1))),
region: this.guessRegionForBuilder(endpoint.host),
}));
// Filter by requested region
const regionalServers = allServers.filter((server) => server.region === region);
if (regionalServers.length === 0) {
console.warn(
`No servers found for region '${region}'. Using all available servers.`,
);
this.options.servers = allServers;
} else {
this.options.servers = regionalServers;
}
return this;
}
/**
* Guess region from hostname for builder (helper method)
*/
private guessRegionForBuilder(host: string): string {
const hostname = host.toLowerCase();
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
return 'local';
}
// European indicators
if (
hostname.includes('.de') || hostname.includes('.eu') ||
hostname.includes('europe') ||
hostname.includes('aranguren') || hostname.includes('emzy')
) {
return 'eu';
}
// US indicators
if (
hostname.includes('.us') || hostname.includes('qtornado') ||
hostname.includes('fortress')
) {
return 'us';
}
// Asia indicators
if (
hostname.includes('.jp') || hostname.includes('.cn') ||
hostname.includes('.sg') ||
hostname.includes('asia')
) {
return 'asia';
}
return 'global';
}
/**
* Get network name for builder
*/
private getNetworkName(): string {
if (this.network && typeof this.network === 'object') {
if ('bech32' in this.network) {
switch (this.network.bech32) {
case 'bc':
return 'mainnet';
case 'tb':
return 'testnet';
case 'bcrt':
return 'regtest';
}
}
}
return 'mainnet';
}
/**
* Build the provider
*/
build(): ElectrumXPooledProvider {
return new ElectrumXPooledProvider({
network: this.network,
timeout: 30000,
retries: 3,
retryDelay: 1000,
maxRetryDelay: 10000,
...this.options,
});
}
}