From 00bcb8ea88fa4b0cc12c7ecb794b042990957f43 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 22:42:10 -0500 Subject: [PATCH] Refactoring docs and class implementations --- src/coinbase/authenticator.ts | 9 ++++--- src/coinbase/coinbase.ts | 33 ++++++++++++++++-------- src/coinbase/errors.ts | 21 ++++++++++++--- src/coinbase/tests/authenticator_test.ts | 10 +++---- src/coinbase/tests/coinbase_test.ts | 24 ++++++++++------- src/coinbase/types.ts | 6 ++--- src/coinbase/utils.ts | 7 ++++- 7 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/coinbase/authenticator.ts b/src/coinbase/authenticator.ts index e25fb5b6..83dfcee5 100644 --- a/src/coinbase/authenticator.ts +++ b/src/coinbase/authenticator.ts @@ -23,8 +23,9 @@ export class CoinbaseAuthenticator { /** * Middleware to intercept requests and add JWT to Authorization header. - * @param {MiddlewareRequestType} config - The request configuration. - * @returns {MiddlewareRequestType} The request configuration with the Authorization header added. + * @param {InternalAxiosRequestConfig} config - The request configuration. + * @param {boolean} debugging - Flag to enable debugging. + * @returns {Promise} The request configuration with the Authorization header added. * @throws {InvalidAPIKeyFormat} If JWT could not be built. */ async authenticateRequest( @@ -45,7 +46,7 @@ export class CoinbaseAuthenticator { * Builds the JWT for the given API endpoint URL. * @param {string} url - URL of the API endpoint. * @param {string} method - HTTP method of the request. - * @returns {string} JWT token. + * @returns {Promise} JWT token. * @throws {InvalidAPIKeyFormat} If the private key is not in the correct format. */ async buildJWT(url: string, method = "GET"): Promise { @@ -108,7 +109,7 @@ export class CoinbaseAuthenticator { /** * Generates a random nonce for the JWT. - * @returns {string} + * @returns {string} The generated nonce. */ private nonce(): string { const range = "0123456789"; diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 20bec0ef..71271bb1 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -7,7 +7,7 @@ import { CoinbaseAuthenticator } from "./authenticator"; import { ApiClients } from "./types"; import { User } from "./user"; import { logApiResponse } from "./utils"; -import { InvalidAPIKeyFormat, InvalidConfiguration } from "./errors"; +import { InvalidAPIKeyFormat, InternalError, InvalidConfiguration } from "./errors"; // The Coinbase SDK. export class Coinbase { @@ -20,7 +20,7 @@ export class Coinbase { * @param {string} privateKey - The private key associated with the API key. * @param {boolean} debugging - If true, logs API requests and responses to the console. * @param {string} basePath - The base path for the API. - * @throws {InvalidConfiguration} If the configuration is invalid. + * @throws {InternalError} If the configuration is invalid. * @throws {InvalidAPIKeyFormat} If not able to create JWT token. */ constructor( @@ -30,7 +30,7 @@ export class Coinbase { basePath: string = BASE_PATH, ) { if (apiKeyName === "" || privateKey === "") { - throw new InvalidConfiguration("Invalid configuration: privateKey or apiKeyName is empty"); + throw new InternalError("Invalid configuration: privateKey or apiKeyName is empty"); } const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey); const config = new Configuration({ @@ -49,36 +49,47 @@ export class Coinbase { * @param {string} filePath - The path to the JSON file containing the API key and private key. * @returns {Coinbase} A new instance of the Coinbase SDK. * @throws {InvalidAPIKeyFormat} If the file does not exist or the configuration values are missing/invalid. + * @throws {InvalidConfiguration} If the configuration is invalid. + * @throws {InvalidAPIKeyFormat} If not able to create JWT token. */ static fromJsonConfig( filePath: string = "coinbase_cloud_api_key.json", - debugging = false, + debugging: boolean = false, basePath: string = BASE_PATH, ): Coinbase { if (!fs.existsSync(filePath)) { - throw new InvalidAPIKeyFormat(`Invalid configuration: file not found at ${filePath}`); + throw new InvalidConfiguration(`Invalid configuration: file not found at ${filePath}`); } try { const data = fs.readFileSync(filePath, "utf8"); - const config = JSON.parse(data); + const config = JSON.parse(data) as { name: string; privateKey: string }; if (!config.name || !config.privateKey) { throw new InvalidAPIKeyFormat("Invalid configuration: missing configuration values"); } - // Return a new instance of Coinbase return new Coinbase(config.name, config.privateKey, debugging, basePath); } catch (e) { - throw new InvalidAPIKeyFormat(`Not able to parse the configuration file`); + if (e instanceof SyntaxError) { + throw new InvalidAPIKeyFormat("Not able to parse the configuration file"); + } else { + throw new InvalidAPIKeyFormat( + `An error occurred while reading the configuration file: ${(e as Error).message}`, + ); + } } } /** * Returns User model for the default user. * @returns {User} The default user. - * @throws {Error} If the user is not found or HTTP request fails. + * @throws {InternalError} If the request fails. */ async defaultUser(): Promise { - const user = await this.apiClients.user?.getCurrentUser(); - return new User(user?.data as UserModel, this.apiClients); + try { + const userResponse = await this.apiClients.user!.getCurrentUser(); + return new User(userResponse.data as UserModel, this.apiClients); + } catch (error) { + throw new InternalError(`Failed to retrieve user: ${(error as Error).message}`); + } } } diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index 50391ff1..f95fa262 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -1,20 +1,35 @@ export class InvalidAPIKeyFormat extends Error { - constructor(message: string = "Invalid API key format") { + static DEFAULT_MESSAGE = "Invalid API key format"; + + constructor(message: string = InvalidAPIKeyFormat.DEFAULT_MESSAGE) { super(message); this.name = "InvalidAPIKeyFormat"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidAPIKeyFormat); + } } } export class InternalError extends Error { - constructor(message: string = "Internal Error") { + static DEFAULT_MESSAGE = "Internal Error"; + + constructor(message: string = InternalError.DEFAULT_MESSAGE) { super(message); this.name = "InternalError"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InternalError); + } } } export class InvalidConfiguration extends Error { - constructor(message: string = "Invalid configuration") { + static DEFAULT_MESSAGE = "Invalid configuration"; + + constructor(message: string = InvalidConfiguration.DEFAULT_MESSAGE) { super(message); this.name = "InvalidConfiguration"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidConfiguration); + } } } diff --git a/src/coinbase/tests/authenticator_test.ts b/src/coinbase/tests/authenticator_test.ts index 54205a50..3782cc6f 100644 --- a/src/coinbase/tests/authenticator_test.ts +++ b/src/coinbase/tests/authenticator_test.ts @@ -13,7 +13,7 @@ describe("Authenticator tests", () => { const keys = require(filePath); const authenticator = new CoinbaseAuthenticator(keys.name, keys.privateKey); - it("should raise InvalidConfiguration error", async () => { + it("should raise InvalidConfiguration error for invalid config", async () => { const invalidConfig = { method: "GET", url: "https://api.cdp.coinbase.com/platform/v1/users/me", @@ -27,15 +27,11 @@ describe("Authenticator tests", () => { const config = await authenticator.authenticateRequest(VALID_CONFIG); const token = config.headers?.Authorization as string; expect(token).toContain("Bearer "); - // length of the token should be greater than 100 expect(token?.length).toBeGreaterThan(100); }); - it("invalid pem key should raise an error", () => { - const invalidAuthenticator = new CoinbaseAuthenticator( - "test-key", - "-----BEGIN EC PRIVATE KEY-----+L+==\n-----END EC PRIVATE KEY-----\n", - ); + it("invalid pem key should raise an InvalidAPIKeyFormat error", async () => { + const invalidAuthenticator = new CoinbaseAuthenticator("test-key", "-----BEGIN EC KEY-----\n"); expect(invalidAuthenticator.authenticateRequest(VALID_CONFIG)).rejects.toThrow(); }); }); diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index d5c1507c..e94ac0a2 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -3,16 +3,22 @@ import MockAdapter from "axios-mock-adapter"; import axios from "axios"; const axiosMock = new MockAdapter(axios); +const PATH_PREFIX = "./src/coinbase/tests/config"; describe("Coinbase tests", () => { - const PATH_PREFIX = "./src/coinbase/tests/config"; + beforeEach(() => { + axiosMock.reset(); + }); + it("should throw an error if the API key name or private key is empty", () => { - expect(() => new Coinbase("", "")).toThrow("Invalid configuration"); + expect(() => new Coinbase("", "")).toThrow( + "Invalid configuration: privateKey or apiKeyName is empty", + ); }); it("should throw an error if the file does not exist", () => { expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/does-not-exist.json`)).toThrow( - "Invalid configuration", + "Invalid configuration: file not found at ./src/coinbase/tests/config/does-not-exist.json", ); }); @@ -23,7 +29,7 @@ describe("Coinbase tests", () => { it("should throw an error if there is an issue reading the file or parsing the JSON data", () => { expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/invalid.json`)).toThrow( - "Not able to parse the configuration file", + "Invalid configuration: missing configuration values", ); }); @@ -33,8 +39,8 @@ describe("Coinbase tests", () => { ); }); - it("should able to get the default user", async () => { - axiosMock.onGet().reply(200, { + it("should be able to get the default user", async () => { + axiosMock.onGet("https://api.cdp.coinbase.com/platform/v1/users/me").reply(200, { id: 123, }); const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`, true); @@ -44,10 +50,8 @@ describe("Coinbase tests", () => { }); it("should raise an error if the user is not found", async () => { - axiosMock.onGet().reply(404, { - id: 123, - }); + axiosMock.onGet("https://api.cdp.coinbase.com/platform/v1/users/me").reply(404); const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); - expect(cbInstance.defaultUser()).rejects.toThrow("Request failed with status code 404"); + await expect(cbInstance.defaultUser()).rejects.toThrow("Request failed with status code 404"); }); }); diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index bdb22619..d5e0ad34 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -1,4 +1,4 @@ -import { AxiosPromise } from "axios"; +import { AxiosPromise, AxiosRequestConfig } from "axios"; import { User as UserModel } from "./../client/api"; /** @@ -11,14 +11,12 @@ export type UserAPIClient = { * @returns {AxiosPromise} - A promise resolving to the User model. * @throws {Error} If the request fails. */ - getCurrentUser(options?): AxiosPromise; + getCurrentUser(options?: AxiosRequestConfig): AxiosPromise; }; /** * API clients type definition for the Coinbase SDK. * Represents the set of API clients available in the SDK. - * @typedef {Object} ApiClients - * @property {UserAPIClient} [user] - The User API client. */ export type ApiClients = { /** diff --git a/src/coinbase/utils.ts b/src/coinbase/utils.ts index 7b3828d9..42923843 100644 --- a/src/coinbase/utils.ts +++ b/src/coinbase/utils.ts @@ -3,6 +3,7 @@ import { AxiosResponse } from "axios"; /** * Prints Axios response to the console for debugging purposes. * @param response - The Axios response object. + * @param debugging - Flag to enable or disable logging. */ export const logApiResponse = (response: AxiosResponse, debugging = false): AxiosResponse => { if (debugging) { @@ -11,7 +12,11 @@ export const logApiResponse = (response: AxiosResponse, debugging = false): Axio if (typeof response.data === "object") { output = JSON.stringify(response.data, null, 4); } - console.log(`API RESPONSE: ${response.status} ${response.config.url} ${output}`); + + console.log(`API RESPONSE: + Status: ${response.status} + URL: ${response.config.url} + Data: ${output}`); } return response; };