-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding initial version of Coinbase Class #8
Changes from 10 commits
623997a
60a7953
744560b
f044870
ea3e0e5
33fe676
15fa64f
6d43e54
243cd63
75e9318
e3693c6
d6f6b1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,3 +130,4 @@ dist | |
.yarn/build-state.yml | ||
.yarn/install-state.gz | ||
.pnp.* | ||
.DS_Store |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,95 @@ | ||||||
import globalAxios from "axios"; | ||||||
import fs from "fs"; | ||||||
import { UsersApiFactory, User as UserModel } from "../client"; | ||||||
import { BASE_PATH } from "./../client/base"; | ||||||
import { Configuration } from "./../client/configuration"; | ||||||
import { CoinbaseAuthenticator } from "./authenticator"; | ||||||
import { ApiClients } from "./types"; | ||||||
import { User } from "./user"; | ||||||
import { logApiResponse } from "./utils"; | ||||||
import { InvalidAPIKeyFormat, InternalError, InvalidConfiguration } from "./errors"; | ||||||
|
||||||
// The Coinbase SDK. | ||||||
export class Coinbase { | ||||||
apiClients: ApiClients = {}; | ||||||
|
||||||
/** | ||||||
* Initializes the Coinbase SDK. | ||||||
* @constructor | ||||||
* @param {string} apiKeyName - The API key name. | ||||||
* @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 {InternalError} If the configuration is invalid. | ||||||
* @throws {InvalidAPIKeyFormat} If not able to create JWT token. | ||||||
*/ | ||||||
constructor( | ||||||
apiKeyName: string, | ||||||
privateKey: string, | ||||||
debugging = false, | ||||||
basePath: string = BASE_PATH, | ||||||
) { | ||||||
if (apiKeyName === "" || privateKey === "") { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets explicitly say which one is empty? |
||||||
throw new InternalError("Invalid configuration: privateKey or apiKeyName is empty"); | ||||||
} | ||||||
const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey); | ||||||
const config = new Configuration({ | ||||||
basePath: basePath, | ||||||
}); | ||||||
const axiosInstance = globalAxios.create(); | ||||||
axiosInstance.interceptors.request.use(config => | ||||||
coinbaseAuthenticator.authenticateRequest(config, debugging), | ||||||
); | ||||||
axiosInstance.interceptors.response.use(response => logApiResponse(response, debugging)); | ||||||
this.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Reads the API key and private key from a JSON file and initializes the Coinbase SDK. | ||||||
* @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 configureFromJson( | ||||||
filePath: string = "coinbase_cloud_api_key.json", | ||||||
debugging: boolean = false, | ||||||
basePath: string = BASE_PATH, | ||||||
): Coinbase { | ||||||
if (!fs.existsSync(filePath)) { | ||||||
throw new InvalidConfiguration(`Invalid configuration: file not found at ${filePath}`); | ||||||
} | ||||||
try { | ||||||
const data = fs.readFileSync(filePath, "utf8"); | ||||||
const config = JSON.parse(data) as { name: string; privateKey: string }; | ||||||
if (!config.name || !config.privateKey) { | ||||||
throw new InvalidAPIKeyFormat("Invalid configuration: missing configuration values"); | ||||||
} | ||||||
|
||||||
return new Coinbase(config.name, config.privateKey, debugging, basePath); | ||||||
} catch (e) { | ||||||
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 {InternalError} If the request fails. | ||||||
*/ | ||||||
async defaultUser(): Promise<User> { | ||||||
erdimaden marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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}`); | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,35 @@ | ||
export const InvalidConfiguration = new Error("Invalid configuration"); | ||
export const InvalidAPIKeyFormat = new Error("Invalid format of API private key"); | ||
export const InternalError = new Error(`Internal Error`); | ||
export class InvalidAPIKeyFormat extends Error { | ||
static DEFAULT_MESSAGE = "Invalid API key format"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc |
||
|
||
constructor(message: string = InvalidAPIKeyFormat.DEFAULT_MESSAGE) { | ||
super(message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc |
||
this.name = "InvalidAPIKeyFormat"; | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, InvalidAPIKeyFormat); | ||
} | ||
} | ||
} | ||
|
||
export class InternalError extends Error { | ||
static DEFAULT_MESSAGE = "Internal Error"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc |
||
|
||
constructor(message: string = InternalError.DEFAULT_MESSAGE) { | ||
super(message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc |
||
this.name = "InternalError"; | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, InternalError); | ||
} | ||
} | ||
} | ||
|
||
export class InvalidConfiguration extends Error { | ||
static DEFAULT_MESSAGE = "Invalid configuration"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a ticket for Errors but I will add JSDocs for the existing classes |
||
|
||
constructor(message: string = InvalidConfiguration.DEFAULT_MESSAGE) { | ||
super(message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typedoc |
||
this.name = "InvalidConfiguration"; | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, InvalidConfiguration); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Coinbase } from "../coinbase"; | ||
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", () => { | ||
beforeEach(() => { | ||
axiosMock.reset(); | ||
}); | ||
|
||
it("should throw an error if the API key name or private key is empty", () => { | ||
expect(() => new Coinbase("", "")).toThrow( | ||
"Invalid configuration: privateKey or apiKeyName is empty", | ||
); | ||
}); | ||
|
||
it("should throw an error if the file does not exist", () => { | ||
expect(() => Coinbase.configureFromJson(`${PATH_PREFIX}/does-not-exist.json`)).toThrow( | ||
"Invalid configuration: file not found at ./src/coinbase/tests/config/does-not-exist.json", | ||
); | ||
}); | ||
|
||
it("should initialize the Coinbase SDK from a JSON file", () => { | ||
const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); | ||
expect(cbInstance).toBeInstanceOf(Coinbase); | ||
}); | ||
|
||
it("should throw an error if there is an issue reading the file or parsing the JSON data", () => { | ||
expect(() => Coinbase.configureFromJson(`${PATH_PREFIX}/invalid.json`)).toThrow( | ||
"Invalid configuration: missing configuration values", | ||
); | ||
}); | ||
|
||
it("should throw an error if the JSON file is not parseable", () => { | ||
expect(() => Coinbase.configureFromJson(`${PATH_PREFIX}/not_parseable.json`)).toThrow( | ||
"Not able to parse the configuration file", | ||
); | ||
}); | ||
|
||
it("should be able to get the default user", async () => { | ||
axiosMock.onGet().reply(200, { | ||
id: 123, | ||
}); | ||
const cbInstance = Coinbase.configureFromJson( | ||
`${PATH_PREFIX}/coinbase_cloud_api_key.json`, | ||
true, | ||
); | ||
const user = await cbInstance.defaultUser(); | ||
expect(user.getUserId()).toBe(123); | ||
expect(user.toString()).toBe("Coinbase:User{userId: 123}"); | ||
}); | ||
|
||
it("should raise an error if the user is not found", async () => { | ||
axiosMock.onGet().reply(404); | ||
const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); | ||
await expect(cbInstance.defaultUser()).rejects.toThrow( | ||
"Failed to retrieve user: Request failed with status code 404", | ||
); | ||
}); | ||
}); |
erdimaden marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"name": "organizations/ej811111-bf11-4e11-111c-3e3e1e33333b", | ||
"privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"name": 0, | ||
"apiSecret": "" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
not parseable content |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { User } from "./../user"; | ||
import { ApiClients } from "./../types"; | ||
import { User as UserModel } from "./../../client/api"; | ||
|
||
describe("User Class", () => { | ||
let mockUserModel: UserModel; | ||
let mockApiClients: ApiClients; | ||
|
||
beforeEach(() => { | ||
mockUserModel = { | ||
id: "12345", | ||
} as UserModel; | ||
|
||
mockApiClients = {} as ApiClients; | ||
}); | ||
|
||
it("should initialize User instance with a valid user model and API clients, and set the user ID correctly", () => { | ||
const user = new User(mockUserModel, mockApiClients); | ||
expect(user).toBeInstanceOf(User); | ||
expect(user.getUserId()).toBe(mockUserModel.id); | ||
}); | ||
|
||
it("should return a correctly formatted string representation of the User instance", () => { | ||
const user = new User(mockUserModel, mockApiClients); | ||
expect(user.toString()).toBe(`Coinbase:User{userId: ${mockUserModel.id}}`); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When do we use // and when do we use /* style comments? Let's have some consistency around this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added
"multiline-comment-style": ["error", "starred-block"],
to eslint so with this configuration:we use // for single-line comments.
we use /* */ for multi-line comments with a starred block style.
https://eslint.org/docs/latest/rules/multiline-comment-style