UNPKG

@mr-zwets/bchn-api-wrapper

Version:

a Typescript wrapper for interacting with the Bitcoin Cash Node (BCHN) API

94 lines (79 loc) 3.81 kB
import type { RpcClientConfig, RpcRequest } from "./interfaces/interfaces.js"; import { getRandomId, validateAndConstructUrl } from "./utils/utils.js"; import { RetryLimitExceededError } from "./utils/errors.js"; export class BchnRpcClient { private url: string private rpcUser: string private rpcPassword: string private maxRetries: number // number of retries before throwing an exception private retryDelayMs: number // delay between each retry private logger: typeof console // logger private timeoutMs: number // max timeout for each retry constructor(config: RpcClientConfig){ this.url = validateAndConstructUrl(config) if(!config.rpcUser) throw new Error('Need to provide rpcUser in config') if(!config.rpcPassword) throw new Error('Need to provide rpcPassword in config') this.rpcUser = config.rpcUser; this.rpcPassword= config.rpcPassword; // optional config this.maxRetries = config.maxRetries ?? 0; this.retryDelayMs= config.retryDelayMs ?? 100; this.logger = config.logger ?? console; this.timeoutMs = config.timeoutMs ?? 5000; } async request<T extends RpcRequest>( endpoint: T['method'], ...params: T['params'] ): Promise<T['response']> { const auth = Buffer.from(`${this.rpcUser}:${this.rpcPassword}`).toString('base64'); // Retry logic for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { // Send the request with a timeout and retries const response = await fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` }, body: JSON.stringify({ jsonrpc: '2.0', method: endpoint, params, id: getRandomId() }), signal: AbortSignal.timeout(this.timeoutMs), }); const result = await response.json(); // Handle response errors if (!response.ok || result.error) { throw new Error(`Error: ${result.error?.message || response.statusText}`); } return result.result as T['response']; // Return the result if successful } catch (error) { let errorMessage: string | undefined // Check if the error is due to timeout or other fetch-related issues if(typeof error == 'string'){ errorMessage = error this.logger.error(error) } else if (error instanceof DOMException && error.name === 'TimeoutError') { // If error is an instance DOMException TimeoutError errorMessage = error.message this.logger.error(`Request timed out after ${this.timeoutMs} ms`); } else if (error instanceof Error) { // If error is an instance of Error, you can safely access its properties errorMessage = error.message this.logger.error(`Request failed with error: ${error.message}`); } // Retry if allowed if (attempt < this.maxRetries) { this.logger.warn(`Retrying request... (${attempt + 1}/${this.maxRetries})`); await new Promise(res => setTimeout(res, this.retryDelayMs)); // Wait before retrying } else { // If no retries are left, throw the final error throw new RetryLimitExceededError(`Request failed after ${this.maxRetries + 1} attempts: ${errorMessage}`); } } } // This line ensures TypeScript is satisfied that a value will always be returned, but // it should never be reached if the retries fail, as the last attempt should throw an error. throw new Error('Request failed unexpectedly'); } }