Skip to content

Commit

Permalink
Refactoring docs and class implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
erdimaden committed May 14, 2024
1 parent 15fa64f commit 00bcb8e
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 40 deletions.
9 changes: 5 additions & 4 deletions src/coinbase/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternalAxiosRequestConfig>} The request configuration with the Authorization header added.
* @throws {InvalidAPIKeyFormat} If JWT could not be built.
*/
async authenticateRequest(
Expand All @@ -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<string>} JWT token.
* @throws {InvalidAPIKeyFormat} If the private key is not in the correct format.
*/
async buildJWT(url: string, method = "GET"): Promise<string> {
Expand Down Expand Up @@ -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";
Expand Down
33 changes: 22 additions & 11 deletions src/coinbase/coinbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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({
Expand All @@ -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<User> {
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}`);
}
}
}
21 changes: 18 additions & 3 deletions src/coinbase/errors.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
10 changes: 3 additions & 7 deletions src/coinbase/tests/authenticator_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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();
});
});
24 changes: 14 additions & 10 deletions src/coinbase/tests/coinbase_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});

Expand All @@ -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",
);
});

Expand All @@ -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);
Expand All @@ -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");
});
});
6 changes: 2 additions & 4 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosPromise } from "axios";
import { AxiosPromise, AxiosRequestConfig } from "axios";
import { User as UserModel } from "./../client/api";

/**
Expand All @@ -11,14 +11,12 @@ export type UserAPIClient = {
* @returns {AxiosPromise<UserModel>} - A promise resolving to the User model.
* @throws {Error} If the request fails.
*/
getCurrentUser(options?): AxiosPromise<UserModel>;
getCurrentUser(options?: AxiosRequestConfig): AxiosPromise<UserModel>;
};

/**
* 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 = {
/**
Expand Down
7 changes: 6 additions & 1 deletion src/coinbase/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
};

0 comments on commit 00bcb8e

Please sign in to comment.