@solsdk/jito-ts
Version:
## What is it and why do you need it?
263 lines • 12.2 kB
JavaScript
;
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