opnet
Version:
The perfect library for building Bitcoin-based applications.
496 lines (388 loc) • 12.6 kB
Markdown
# Advanced Configuration
This guide covers advanced provider configuration including request batching, error handling, retry logic, and connection pooling.
## Overview
```mermaid
flowchart TB
subgraph Config["Advanced Configuration"]
Batch[Request Batching]
Retry[Retry Logic]
Pool[Connection Pooling]
Error[Error Handling]
end
subgraph Provider["Provider"]
Fetch[HTTP Fetcher]
end
Batch --> Provider
Retry --> Provider
Pool --> Fetch
Error --> Provider
```
---
## Request Batching
While the provider handles individual requests, you can batch operations for efficiency:
### Parallel Requests
```typescript
// Batch multiple independent requests
const [block, balance, utxos] = await Promise.all([
provider.getBlockNumber(),
provider.getBalance('bc1q...'),
provider.utxoManager.getUTXOs({ address: 'bc1q...' }),
]);
```
### Sequential with Dependencies
```typescript
// When requests depend on each other
const blockNumber = await provider.getBlockNumber();
const block = await provider.getBlock(blockNumber);
const transactions = await Promise.all(
block.transactions?.map((txId) => provider.getTransaction(txId)) || []
);
```
### Batch Pattern for Multiple Addresses
```typescript
// Efficient batch balance check
async function getBalancesBatch(addresses: string[]): Promise<Map<string, bigint>> {
const balanceMap = await provider.getBalances(addresses, true);
return new Map(Object.entries(balanceMap));
}
const addresses = ['bc1q...', 'bc1p...', 'bc1r...'];
const balances = await getBalancesBatch(addresses);
```
---
## Error Handling
### OPNet Error Types
```typescript
import { OPNetError } from 'opnet';
try {
await provider.getBlock(999999999);
} catch (error) {
if (error instanceof OPNetError) {
console.error('OPNet Error');
console.error(' Message:', error.message);
console.error(' Code:', error.code);
} else if (error instanceof Error) {
// Network or other error
console.error('Error:', error.message);
}
}
```
### Common Error Scenarios
| Error Type | Cause | Solution |
|------------|-------|----------|
| Timeout | Request took too long | Increase timeout or retry |
| Connection Refused | Server unavailable | Check URL, retry later |
| Invalid Response | Malformed RPC response | Check request parameters |
| Block Not Found | Non-existent block | Verify block number exists |
| Revert | Contract execution failed | Check simulation first |
### Error Handling Patterns
```typescript
// Comprehensive error handling
async function safeGetBlock(height: bigint) {
try {
return await provider.getBlock(height);
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('timeout')) {
console.warn('Request timed out, retrying...');
return await provider.getBlock(height);
}
if (error.message.includes('not found')) {
console.warn('Block not found');
return null;
}
}
throw error; // Re-throw unknown errors
}
}
```
---
## Retry Logic
Implement retry logic for transient failures:
### Simple Retry
```typescript
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
console.warn(`Attempt ${attempt} failed: ${lastError.message}`);
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, delayMs * attempt));
}
}
}
throw lastError;
}
// Usage
const block = await withRetry(() => provider.getBlock(height));
```
### Exponential Backoff
```typescript
async function withExponentialBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 5,
baseDelayMs: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delay = baseDelayMs * Math.pow(2, attempt);
console.warn(`Attempt ${attempt + 1} failed, waiting ${delay}ms`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}
```
### Retry with Circuit Breaker
```typescript
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private readonly threshold = 5;
private readonly resetTimeMs = 30000;
async execute<T>(operation: () => Promise<T>): Promise<T> {
// Check if circuit is open
if (this.failures >= this.threshold) {
if (Date.now() - this.lastFailure < this.resetTimeMs) {
throw new Error('Circuit breaker is open');
}
// Reset after timeout
this.failures = 0;
}
try {
const result = await operation();
this.failures = 0; // Reset on success
return result;
} catch (error) {
this.failures++;
this.lastFailure = Date.now();
throw error;
}
}
}
// Usage
const breaker = new CircuitBreaker();
const block = await breaker.execute(() => provider.getBlock(height));
```
---
## Connection Pooling
The JSON-RPC provider uses undici for HTTP requests with built-in connection pooling:
### Default Pool Configuration
```typescript
const provider = new JSONRpcProvider({
url,
network,
timeout: 20_000,
fetcherConfigurations: {
keepAliveTimeout: 30_000, // Socket keep-alive
keepAliveTimeoutThreshold: 30_000,
connections: 128, // Max connections
pipelining: 2, // Pipelined requests
},
});
```
### High-Performance Configuration
```typescript
// For high-throughput applications
const provider = new JSONRpcProvider({
url,
network,
timeout: 30_000,
fetcherConfigurations: {
keepAliveTimeout: 60_000, // Longer keep-alive
keepAliveTimeoutThreshold: 60_000,
connections: 256, // More connections
pipelining: 10, // More pipelining
},
});
```
### Configuration Options Explained
| Option | Description | Default |
|--------|-------------|---------|
| `keepAliveTimeout` | How long sockets stay open without activity | 30s |
| `keepAliveTimeoutThreshold` | Threshold before closing keep-alive sockets | 30s |
| `connections` | Maximum concurrent connections per server | 128 |
| `pipelining` | Maximum requests to pipeline per connection | 2 |
---
## Timeout Configuration
### Request-Level Timeout
```typescript
// Provider-wide timeout
const provider = new JSONRpcProvider({
url,
network,
timeout: 60_000, // 60 second timeout
});
```
### Operation-Specific Timeout
```typescript
// Wrap with custom timeout
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId!);
}
}
// Usage
const block = await withTimeout(
provider.getBlock(height),
5000 // 5 second timeout for this specific call
);
```
---
## Request Rate Limiting
Implement rate limiting to avoid overwhelming nodes:
```typescript
import pLimit from 'p-limit';
// Limit concurrent requests
const limit = pLimit(10); // Max 10 concurrent
async function rateLimitedFetch(addresses: string[]) {
return await Promise.all(
addresses.map((addr) =>
limit(() => provider.getBalance(addr))
)
);
}
```
### Token Bucket Rate Limiter
```typescript
class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private readonly maxTokens: number = 10,
private readonly refillRate: number = 1 // tokens per second
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens > 0) {
this.tokens--;
return;
}
// Wait for token
const waitTime = (1 / this.refillRate) * 1000;
await new Promise((r) => setTimeout(r, waitTime));
return this.acquire();
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const newTokens = elapsed * this.refillRate;
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
this.lastRefill = now;
}
}
// Usage
const limiter = new RateLimiter(10, 5); // 10 tokens, 5/second refill
async function rateLimitedCall<T>(operation: () => Promise<T>): Promise<T> {
await limiter.acquire();
return operation();
}
```
---
## Complete Production Example
```typescript
import { JSONRpcProvider, OPNetError } from 'opnet';
import { networks } from '@btc-vision/bitcoin';
import pLimit from 'p-limit';
// Production-ready provider wrapper
class OPNetClient {
private provider: JSONRpcProvider;
private limiter = pLimit(20);
private retryConfig = { maxRetries: 3, baseDelay: 1000 };
constructor(url: string, network: typeof networks.bitcoin) {
this.provider = new JSONRpcProvider({
url,
network,
timeout: 60_000,
fetcherConfigurations: {
keepAliveTimeout: 60_000,
connections: 256,
pipelining: 4,
},
});
}
async getBlock(height: bigint) {
return this.execute(() => this.provider.getBlock(height));
}
async getBalance(address: string) {
return this.execute(() => this.provider.getBalance(address));
}
async getBalances(addresses: string[]) {
return this.execute(() =>
this.provider.getBalances(addresses, true)
);
}
async close() {
await this.provider.close();
}
private async execute<T>(operation: () => Promise<T>): Promise<T> {
return this.limiter(async () => {
let lastError: Error | undefined;
for (let i = 0; i < this.retryConfig.maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (error instanceof OPNetError) {
// Don't retry certain errors.
// Note: OPNetError.code values are WebSocket error codes,
// not HTTP status codes. Check for specific error codes
// relevant to your application.
throw error;
}
const delay = this.retryConfig.baseDelay * Math.pow(2, i);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
});
}
}
// Usage
const client = new OPNetClient('https://mainnet.opnet.org', networks.bitcoin);
try {
const block = await client.getBlock(800000n);
console.log('Block:', block.hash);
} finally {
await client.close();
}
```
---
## Best Practices
1. **Always Handle Errors**: Wrap provider calls in try/catch
2. **Implement Retries**: Use exponential backoff for transient failures
3. **Rate Limit**: Don't overwhelm nodes with requests
4. **Configure Timeouts**: Set appropriate timeouts for your use case
5. **Close Connections**: Always clean up provider resources
6. **Monitor Performance**: Track request times and failure rates
---
## Next Steps
- [Contract Interactions](../contracts/overview.md) - Working with smart contracts
- [UTXO Management](../bitcoin/utxos.md) - Managing Bitcoin UTXOs
---
[← Previous: Internal Caching](./internal-caching.md) | [Next: Contract Overview →](../contracts/overview.md)