UNPKG

@solsdk/jito-ts

Version:

## What is it and why do you need it?

263 lines 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.searcherClient = exports.SearcherClient = exports.SearcherClientError = void 0; const grpc_js_1 = require("@grpc/grpc-js"); const auth_1 = require("../../gen/block-engine/auth"); const searcher_1 = require("../../gen/block-engine/searcher"); const auth_2 = require("./auth"); const utils_1 = require("./utils"); const utils_2 = require("@nealireverse_dev/utils"); class SearcherClientError extends Error { constructor(code, message, details) { super(`${message}${details ? `: ${details}` : ''}`); this.code = code; this.details = details; this.name = 'SearcherClientError'; } } exports.SearcherClientError = SearcherClientError; class SearcherClient { constructor(client) { this.client = client; this.retryOptions = Object.freeze({ maxRetries: 3, baseDelay: 1000, maxDelay: 10000, factor: 2, }); } handleError(e) { const errorDetails = e.details || 'No additional details provided'; switch (e.code) { case grpc_js_1.status.OK: return new SearcherClientError(e.code, 'Unexpected OK status in error', errorDetails); case grpc_js_1.status.CANCELLED: return new SearcherClientError(e.code, 'The operation was cancelled', errorDetails); case grpc_js_1.status.UNKNOWN: return new SearcherClientError(e.code, 'Unknown error', errorDetails); case grpc_js_1.status.INVALID_ARGUMENT: return new SearcherClientError(e.code, 'Invalid argument provided', errorDetails); case grpc_js_1.status.DEADLINE_EXCEEDED: return new SearcherClientError(e.code, 'Deadline exceeded', errorDetails); case grpc_js_1.status.NOT_FOUND: return new SearcherClientError(e.code, 'Requested entity not found', errorDetails); case grpc_js_1.status.ALREADY_EXISTS: return new SearcherClientError(e.code, 'The entity already exists', errorDetails); case grpc_js_1.status.PERMISSION_DENIED: return new SearcherClientError(e.code, 'Lacking required permission', errorDetails); case grpc_js_1.status.RESOURCE_EXHAUSTED: return new SearcherClientError(e.code, 'Resource has been exhausted', errorDetails); case grpc_js_1.status.FAILED_PRECONDITION: return new SearcherClientError(e.code, 'Operation rejected, system not in correct state', errorDetails); case grpc_js_1.status.ABORTED: return new SearcherClientError(e.code, 'The operation was aborted', errorDetails); case grpc_js_1.status.OUT_OF_RANGE: return new SearcherClientError(e.code, 'Operation attempted past the valid range', errorDetails); case grpc_js_1.status.UNIMPLEMENTED: return new SearcherClientError(e.code, 'Operation not implemented or supported', errorDetails); case grpc_js_1.status.INTERNAL: return new SearcherClientError(e.code, 'Internal error', errorDetails); case grpc_js_1.status.UNAVAILABLE: let unavailableMessage = 'The service is currently unavailable'; if (errorDetails.includes('upstream connect error or disconnect/reset before headers')) { unavailableMessage = 'Service unavailable: Envoy overloaded'; } else if (errorDetails.toLowerCase().includes('dns resolution failed')) { unavailableMessage = 'Service unavailable: DNS resolution failed'; } else if (errorDetails.toLowerCase().includes('ssl handshake failed')) { unavailableMessage = 'Service unavailable: SSL handshake failed'; } else if (errorDetails.toLowerCase().includes('connection refused')) { unavailableMessage = 'Service unavailable: Connection refused'; } else if (errorDetails.toLowerCase().includes('network unreachable')) { unavailableMessage = 'Service unavailable: Network is unreachable'; } else if (errorDetails.toLowerCase().includes('timeout')) { unavailableMessage = 'Service unavailable: Connection timed out'; } return new SearcherClientError(e.code, unavailableMessage, errorDetails); case grpc_js_1.status.DATA_LOSS: return new SearcherClientError(e.code, 'Unrecoverable data loss or corruption', errorDetails); case grpc_js_1.status.UNAUTHENTICATED: return new SearcherClientError(e.code, 'Request not authenticated', errorDetails); default: return new SearcherClientError(grpc_js_1.status.UNKNOWN, `Unexpected error: ${e.message}`, errorDetails); } } async retryWithBackoff(operation, retries = 0) { try { return await operation(); } catch (error) { if (retries >= this.retryOptions.maxRetries || !this.isRetryableError(error)) { return (0, utils_1.Err)(error); } const delay = Math.min(this.retryOptions.baseDelay * Math.pow(this.retryOptions.factor, retries), this.retryOptions.maxDelay); console.warn(`Operation failed. Retrying in ${delay}ms... (Attempt ${retries + 1} of ${this.retryOptions.maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.retryWithBackoff(operation, retries + 1); } } isRetryableError(error) { if (error instanceof SearcherClientError) { if (error.code === grpc_js_1.status.UNAVAILABLE) { const nonRetryableMessages = [ 'Service unavailable: DNS resolution failed', 'Service unavailable: SSL handshake failed' ]; return !nonRetryableMessages.some(msg => error.message.includes(msg)); } const retryableCodes = [ grpc_js_1.status.UNAVAILABLE, grpc_js_1.status.RESOURCE_EXHAUSTED, grpc_js_1.status.DEADLINE_EXCEEDED, ]; return retryableCodes.includes(error.code); } return false; } /** * Submits a bundle to the block-engine. * * @param bundle - The Bundle object to be sent. * @returns A Promise that resolves to the bundle's UUID (string) on successful submission. * @throws A ServiceError if there's an issue with the server while sending the bundle. */ async sendBundle(bundle) { return this.retryWithBackoff(() => { return new Promise((resolve) => { this.client.sendBundle({ bundle }, (e, resp) => { if (e) { resolve((0, utils_1.Err)(this.handleError(e))); } else { resolve((0, utils_1.Ok)(resp.uuid)); } }); }); }); } /** * Retrieves tip accounts from the server. * * @returns A Promise that resolves to an array of account strings (usually public keys). * @throws A ServiceError if there's an issue with the server while fetching tip accounts. */ async getTipAccounts() { return this.retryWithBackoff(() => { return new Promise((resolve) => { this.client.getTipAccounts({}, (e, resp) => { if (e) { resolve((0, utils_1.Err)(this.handleError(e))); } else { resolve((0, utils_1.Ok)(resp.accounts)); } }); }); }); } /** * Retrieves connected leaders (validators) from the server. * * @returns A Promise that resolves to a map, where keys are validator identity keys (usually public keys), and values are SlotList objects. * @throws A ServiceError if there's an issue with the server while fetching connected leaders. */ async getConnectedLeaders() { return this.retryWithBackoff(() => { return new Promise((resolve) => { this.client.getConnectedLeaders({}, async (e, resp) => { if (e) { resolve((0, utils_1.Err)(this.handleError(e))); } else { resolve((0, utils_1.Ok)(resp.connectedValidators)); } }); }); }); } /** * Returns the next scheduled leader connected to the block-engine. * * @returns A Promise that resolves with an object containing: * - currentSlot: The current slot number the backend is on * - nextLeaderSlot: The slot number of the next scheduled leader * - nextLeaderIdentity: The identity of the next scheduled leader * @throws A ServiceError if there's an issue with the server while fetching the next scheduled leader. */ async getNextScheduledLeader() { return this.retryWithBackoff(() => { return new Promise((resolve) => { this.client.getNextScheduledLeader({ regions: [], }, async (e, resp) => { if (e) { resolve((0, utils_1.Err)(this.handleError(e))); } else { resolve((0, utils_1.Ok)(resp)); } }); }); }); } /** * Triggers the provided callback on BundleResult updates. * * @param successCallback - A callback function that receives the BundleResult updates * @param errorCallback - A callback function that receives the stream error (Error) * @returns A function to cancel the subscription */ onBundleResult(successCallback, errorCallback) { const stream = this.client.subscribeBundleResults({}); stream.on('readable', () => { const msg = stream.read(1); if (msg) { successCallback(msg); } }); stream.on('error', e => { errorCallback(new Error(`Stream error: ${e.message}`)); }); return () => stream.cancel(); } /** * Yields on bundle results. * * @param onError - A callback function that receives the stream error (Error) * @returns An async generator that yields BundleResult updates */ async *bundleResults(onError) { const stream = this.client.subscribeBundleResults({}); stream.on('error', e => { onError(e); }); for await (const bundleResult of stream) { yield bundleResult; } } } exports.SearcherClient = SearcherClient; /** * Creates and returns a SearcherClient instance. * * @param url - The URL of the SearcherService * @param authKeypair - Optional Keypair authorized for the block engine * @param grpcOptions - Optional configuration options for the gRPC client * @returns SearcherClient - An instance of the SearcherClient */ const searcherClient = (url, authKeypair, grpcOptions) => { if (authKeypair) { const authProvider = new auth_2.AuthProvider(new auth_1.AuthServiceClient(url, grpc_js_1.ChannelCredentials.createSsl()), utils_2.Keypair.from({ keypair: authKeypair })); const client = new searcher_1.SearcherServiceClient(url, grpc_js_1.ChannelCredentials.createSsl(), { interceptors: [(0, auth_2.authInterceptor)(authProvider)], ...grpcOptions }); return new SearcherClient(client); } else { return new SearcherClient(new searcher_1.SearcherServiceClient(url, grpc_js_1.ChannelCredentials.createSsl(), grpcOptions)); } }; exports.searcherClient = searcherClient; //# sourceMappingURL=searcher.js.map