All files / src/blockfrost BlockfrostClient.ts

89.28% Statements 25/28
70% Branches 7/10
87.5% Functions 7/8
91.66% Lines 22/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 9218x 18x 18x                                                   18x 3x   3x 3x       18x                 2x 2x 2x                 2x 2x                 2x 1x 1x         1x 1x 1x   1x       1x 1x                  
import { CustomError } from 'ts-custom-error';
import { catchError, firstValueFrom, switchMap, throwError } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
 
export type BlockfrostClientConfig = {
  projectId?: string;
  baseUrl: string;
  apiVersion?: string;
};
 
export type RateLimiter = {
  schedule: <T>(task: () => Promise<T>) => Promise<T>;
};
 
export type BlockfrostClientDependencies = {
  /**
   * Rate limiter from npm: https://www.npmjs.com/package/bottleneck
   *
   * new Bottleneck({
   *   reservoir: DEFAULT_BLOCKFROST_RATE_LIMIT_CONFIG.size,
   *   reservoirIncreaseAmount: DEFAULT_BLOCKFROST_RATE_LIMIT_CONFIG.increaseAmount,
   *   reservoirIncreaseInterval: DEFAULT_BLOCKFROST_RATE_LIMIT_CONFIG.increaseInterval,
   *   reservoirIncreaseMaximum: DEFAULT_BLOCKFROST_RATE_LIMIT_CONFIG.size
   * })
   */
  rateLimiter: RateLimiter;
};
 
export class BlockfrostError extends CustomError {
  constructor(public status?: number, public body?: string, public innerError?: unknown) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const message: string | null = body || (innerError as any)?.message;
    super(`Blockfrost error with status '${status}': ${message}`);
  }
}
 
export class BlockfrostClient {
  private rateLimiter: RateLimiter;
  private baseUrl: string;
  private requestInit: RequestInit;
 
  constructor(
    { apiVersion, projectId, baseUrl }: BlockfrostClientConfig,
    { rateLimiter }: BlockfrostClientDependencies
  ) {
    this.rateLimiter = rateLimiter;
    this.requestInit = projectId ? { headers: { project_id: projectId } } : {};
    this.baseUrl = apiVersion ? `${baseUrl}/api/${apiVersion}` : `${baseUrl}`;
  }
 
  /**
   * @param endpoint e.g. 'blocks/latest'
   * @param requestInit request options
   * @throws {BlockfrostError}
   */
  public request<T>(endpoint: string, requestInit?: RequestInit): Promise<T> {
    return this.rateLimiter.schedule(() =>
      firstValueFrom(
        fromFetch(`${this.baseUrl}/${endpoint}`, {
          ...this.requestInit,
          ...requestInit,
          headers: requestInit?.headers
            ? { ...this.requestInit.headers, ...requestInit.headers }
            : this.requestInit.headers
        }).pipe(
          switchMap(async (response): Promise<T> => {
            if (response.ok) {
              try {
                return await response.json();
              } catch {
                throw new BlockfrostError(response.status, 'Failed to parse json');
              }
            }
            try {
              const responseBody = await response.text();
              throw new BlockfrostError(response.status, responseBody);
            } catch {
              throw new BlockfrostError(response.status);
            }
          }),
          catchError((err) => {
            if (err instanceof BlockfrostError) {
              return throwError(() => err);
            }
            return throwError(() => new BlockfrostError(undefined, undefined, err));
          })
        )
      )
    );
  }
}