From 623997ab157be0509e4e466bc7f68a5b6315093f Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 17:00:24 -0500 Subject: [PATCH 01/12] Adding initial version of Coinbase Class --- .gitignore | 1 + package.json | 1 + src/coinbase/coinbase.ts | 73 +++++++++++++++++++ src/coinbase/tests/coinbase_test.ts | 53 ++++++++++++++ .../tests/config/coinbase_cloud_api_key.json | 4 + src/coinbase/tests/config/invalid.json | 4 + src/coinbase/tests/config/not_parseable.json | 1 + src/coinbase/types.ts | 8 ++ src/coinbase/user.ts | 19 +++++ yarn.lock | 65 ++++++++++++++++- 10 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/coinbase/coinbase.ts create mode 100644 src/coinbase/tests/coinbase_test.ts create mode 100644 src/coinbase/tests/config/coinbase_cloud_api_key.json create mode 100644 src/coinbase/tests/config/invalid.json create mode 100644 src/coinbase/tests/config/not_parseable.json create mode 100644 src/coinbase/types.ts create mode 100644 src/coinbase/user.ts diff --git a/.gitignore b/.gitignore index 264a5b03..e6ed75ed 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +.DS_Store diff --git a/package.json b/package.json index 87f2cd8b..3f7dc00c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/secp256k1": "^4.0.6", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", + "axios-mock-adapter": "^1.22.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts new file mode 100644 index 00000000..14bc294c --- /dev/null +++ b/src/coinbase/coinbase.ts @@ -0,0 +1,73 @@ +import globalAxios from "axios"; +import fs from "fs"; +import { UsersApiFactory } from "../client"; +import { BASE_PATH } from "./../client/base"; +import { Configuration } from "./../client/configuration"; +import { CoinbaseAuthenticator } from "./authenticator"; +import { InvalidConfiguration } from "./errors"; +import { ApiClients } from "./types"; +import { User } from "./user"; + +// 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. + */ + constructor(apiKeyName: string, privateKey: string) { + if (apiKeyName === "" || privateKey === "") { + throw InvalidConfiguration; + } + const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey); + const config = new Configuration({ + basePath: BASE_PATH, + }); + const axiosInstance = globalAxios.create(); + axiosInstance.interceptors.request.use(config => + coinbaseAuthenticator.authenticateRequest(config), + ); + this.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); + } + + /** + * Reads the API key and private key from a JSON file and returns a new instance of 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. + */ + static fromJsonConfig(filePath: string = "coinbase_cloud_api_key.json"): Coinbase { + /* Read the JSON file for a given path and return a new instance of Coinbase check if the file exists */ + if (!fs.existsSync(filePath)) { + // throw an error if the file does not exist + throw InvalidConfiguration; + } + // read the file and parse the JSON data + try { + // read the file and parse the JSON data + const data = fs.readFileSync(filePath, "utf8"); + + // parse the JSON data + const config = JSON.parse(data); + + // return a new instance of Coinbase + if (!config.apiKeyName || !config.privateKey) { + throw InvalidConfiguration; + } + return new Coinbase(config.apiKeyName, config.privateKey); + } catch (e) { + throw InvalidConfiguration; + } + } + + /** + * Returns the default user. + * @returns {User} The default user. + */ + async defaultUser(): Promise { + const user = await this.apiClients.user?.getCurrentUser(); + return new User(user?.data?.id || "", this.apiClients); + } +} diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts new file mode 100644 index 00000000..91df8982 --- /dev/null +++ b/src/coinbase/tests/coinbase_test.ts @@ -0,0 +1,53 @@ +import { Coinbase } from "../coinbase"; +import MockAdapter from "axios-mock-adapter"; +import axios from "axios"; + +const axiosMock = new MockAdapter(axios); + +describe("Coinbase tests", () => { + const PATH_PREFIX = "./src/coinbase/tests/config"; + it("should throw an error if the API key name or private key is empty", () => { + expect(() => new Coinbase("", "")).toThrow("Invalid configuration"); + }); + + it("should throw an error if the file does not exist", () => { + expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/does-not-exist.json`)).toThrow( + "Invalid configuration", + ); + }); + + it("should initialize the Coinbase SDK from a JSON file", () => { + const cbInstance = Coinbase.fromJsonConfig(`${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.fromJsonConfig(`${PATH_PREFIX}/invalid.json`)).toThrow( + "Invalid configuration", + ); + }); + + it("should throw an error if the JSON file is not parseable", () => { + expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/not_parseable.json`)).toThrow( + "Invalid configuration", + ); + }); + + it("should able to get the default user", async () => { + axiosMock.onGet().reply(200, { + id: 123, + }); + const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + 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, { + id: 123, + }); + const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + expect(cbInstance.defaultUser()).rejects.toThrow("Request failed with status code 404"); + }); +}); diff --git a/src/coinbase/tests/config/coinbase_cloud_api_key.json b/src/coinbase/tests/config/coinbase_cloud_api_key.json new file mode 100644 index 00000000..3f5a6cde --- /dev/null +++ b/src/coinbase/tests/config/coinbase_cloud_api_key.json @@ -0,0 +1,4 @@ +{ + "apiKeyName": "organizations/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b", + "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" +} \ No newline at end of file diff --git a/src/coinbase/tests/config/invalid.json b/src/coinbase/tests/config/invalid.json new file mode 100644 index 00000000..95cc4b47 --- /dev/null +++ b/src/coinbase/tests/config/invalid.json @@ -0,0 +1,4 @@ +{ + "apiKey": 0, + "apiSecret": "" +} diff --git a/src/coinbase/tests/config/not_parseable.json b/src/coinbase/tests/config/not_parseable.json new file mode 100644 index 00000000..5611bc64 --- /dev/null +++ b/src/coinbase/tests/config/not_parseable.json @@ -0,0 +1 @@ +not parseable content \ No newline at end of file diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts new file mode 100644 index 00000000..1305467e --- /dev/null +++ b/src/coinbase/types.ts @@ -0,0 +1,8 @@ +import { AxiosPromise } from "axios"; +import { User as UserModel } from "./../client/api"; + +export type UserAPIClient = { getCurrentUser(options?): AxiosPromise }; + +export type ApiClients = { + user?: UserAPIClient; +}; diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts new file mode 100644 index 00000000..d5cf2b93 --- /dev/null +++ b/src/coinbase/user.ts @@ -0,0 +1,19 @@ +import { ApiClients } from "./types"; + +export class User { + private userId: string = ""; + private client: ApiClients; + + constructor(userId: string, client: ApiClients) { + this.userId = userId; + this.client = client; + } + + public getUserId(): string { + return this.userId; + } + + toString(): string { + return `Coinbase:User{userId: ${this.userId}}`; + } +} diff --git a/yarn.lock b/yarn.lock index 3bdb95aa..2857b342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -965,6 +965,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-sequence-parser@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" + integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1019,6 +1024,14 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +axios-mock-adapter@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d" + integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw== + dependencies: + fast-deep-equal "^3.1.3" + is-buffer "^2.0.5" + axios@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" @@ -1877,6 +1890,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-core-module@^2.13.0: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" @@ -2387,6 +2405,11 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -2458,6 +2481,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -2477,6 +2505,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +marked@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -2529,7 +2562,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.4: +minimatch@^9.0.3, minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== @@ -2881,6 +2914,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== + dependencies: + ansi-sequence-parser "^1.1.0" + jsonc-parser "^3.2.0" + vscode-oniguruma "^1.7.0" + vscode-textmate "^8.0.0" + signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -3095,6 +3138,16 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +typedoc@^0.25.13: + version "0.25.13" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" + integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== + dependencies: + lunr "^2.3.9" + marked "^4.3.0" + minimatch "^9.0.3" + shiki "^0.14.7" + typescript@^5.4.5: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" @@ -3139,6 +3192,16 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +vscode-oniguruma@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" + integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== + +vscode-textmate@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" + integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From 60a7953d215c6c02d0597c1364f07620427b82a9 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 18:23:20 -0500 Subject: [PATCH 02/12] Update src/coinbase/coinbase.ts Co-authored-by: John-peterson-coinbase <98187317+John-peterson-coinbase@users.noreply.github.com> --- src/coinbase/coinbase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 14bc294c..b4b41343 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -53,7 +53,7 @@ export class Coinbase { const config = JSON.parse(data); // return a new instance of Coinbase - if (!config.apiKeyName || !config.privateKey) { + if (!config.name || !config.privateKey) { throw InvalidConfiguration; } return new Coinbase(config.apiKeyName, config.privateKey); From 744560b21d01497c8faebe24a33ad2a542249a09 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 18:48:25 -0500 Subject: [PATCH 03/12] adding JSON config changes --- src/coinbase/coinbase.ts | 2 +- src/coinbase/tests/config/coinbase_cloud_api_key.json | 2 +- src/coinbase/tests/config/invalid.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index b4b41343..cd8a1d1b 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -56,7 +56,7 @@ export class Coinbase { if (!config.name || !config.privateKey) { throw InvalidConfiguration; } - return new Coinbase(config.apiKeyName, config.privateKey); + return new Coinbase(config.name, config.privateKey); } catch (e) { throw InvalidConfiguration; } diff --git a/src/coinbase/tests/config/coinbase_cloud_api_key.json b/src/coinbase/tests/config/coinbase_cloud_api_key.json index 3f5a6cde..9df02493 100644 --- a/src/coinbase/tests/config/coinbase_cloud_api_key.json +++ b/src/coinbase/tests/config/coinbase_cloud_api_key.json @@ -1,4 +1,4 @@ { - "apiKeyName": "organizations/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b", + "name": "organizations/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b", "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" } \ No newline at end of file diff --git a/src/coinbase/tests/config/invalid.json b/src/coinbase/tests/config/invalid.json index 95cc4b47..7aebc5c8 100644 --- a/src/coinbase/tests/config/invalid.json +++ b/src/coinbase/tests/config/invalid.json @@ -1,4 +1,4 @@ { - "apiKey": 0, + "name": 0, "apiSecret": "" } From f044870f9c2c1e1f19d6ff1bbecb78d4d677d320 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 20:52:23 -0500 Subject: [PATCH 04/12] Implementing requested changes and update comments for clarity --- src/coinbase/authenticator.ts | 15 ++++--- src/coinbase/coinbase.ts | 45 +++++++++++-------- src/coinbase/errors.ts | 23 ++++++++-- src/coinbase/tests/authenticator_test.ts | 10 ++--- src/coinbase/tests/coinbase_test.ts | 6 +-- .../tests/config/coinbase_cloud_api_key.json | 2 +- src/coinbase/tests/user_test.ts | 31 +++++++++++++ src/coinbase/types.ts | 6 +++ src/coinbase/user.ts | 12 +++++ src/coinbase/utils.ts | 17 +++++++ 10 files changed, 129 insertions(+), 38 deletions(-) create mode 100644 src/coinbase/tests/user_test.ts create mode 100644 src/coinbase/utils.ts diff --git a/src/coinbase/authenticator.ts b/src/coinbase/authenticator.ts index e64a79b1..7affb6e0 100644 --- a/src/coinbase/authenticator.ts +++ b/src/coinbase/authenticator.ts @@ -29,9 +29,13 @@ export class CoinbaseAuthenticator { */ async authenticateRequest( config: InternalAxiosRequestConfig, + debug = false, ): Promise { const method = config.method?.toString().toUpperCase(); const token = await this.buildJWT(config.url || "", method); + if (debug) { + console.log(`API REQUEST: ${method} ${config.url}`); + } config.headers["Authorization"] = `Bearer ${token}`; config.headers["Content-Type"] = "application/json"; @@ -43,7 +47,7 @@ export class CoinbaseAuthenticator { * @param {string} url - The URI of the API endpoint. * @param {string} method - The HTTP method of the request. * @returns {string} The JWT if successful or throws an error. - * @throws {InternalError} If there is an issue with the private key. + * @throws {InvalidAPIKeyFormat} If there is an issue with the private key. */ async buildJWT(url: string, method = "GET"): Promise { const pemPrivateKey = this.extractPemKey(this.privateKey); @@ -52,10 +56,10 @@ export class CoinbaseAuthenticator { try { privateKey = await JWK.asKey(pemPrivateKey, "pem"); if (privateKey.kty !== "EC") { - throw InternalError; + throw new InvalidAPIKeyFormat("Invalid key type"); } } catch (error) { - throw InternalError; + throw new InvalidAPIKeyFormat("Could not parse the private key"); } const header = { @@ -83,7 +87,7 @@ export class CoinbaseAuthenticator { return result as unknown as string; } catch (err) { - throw InternalError; + throw new InternalError("Could not sign the JWT"); } } @@ -94,14 +98,13 @@ export class CoinbaseAuthenticator { * @throws {InvalidAPIKeyFormat} If the private key string is not in the correct format. */ private extractPemKey(privateKeyString: string): string { - // Remove all newline characters privateKeyString = privateKeyString.replace(/\n/g, ""); if (privateKeyString.startsWith(pemHeader) && privateKeyString.endsWith(pemFooter)) { return privateKeyString; } - throw InvalidAPIKeyFormat; + throw new InvalidAPIKeyFormat("Invalid private key format"); } /** diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index cd8a1d1b..325a4f55 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -4,9 +4,10 @@ import { UsersApiFactory } from "../client"; import { BASE_PATH } from "./../client/base"; import { Configuration } from "./../client/configuration"; import { CoinbaseAuthenticator } from "./authenticator"; -import { InvalidConfiguration } from "./errors"; import { ApiClients } from "./types"; import { User } from "./user"; +import { logApiResponse } from "./utils"; +import { InternalError, InvalidConfiguration } from "./errors"; // The Coinbase SDK. export class Coinbase { @@ -18,18 +19,24 @@ export class Coinbase { * @param {string} apiKeyName - The API key name. * @param {string} privateKey - The private key associated with the API key. */ - constructor(apiKeyName: string, privateKey: string) { + constructor( + apiKeyName: string, + privateKey: string, + debugging = false, + basePath: string = BASE_PATH, + ) { if (apiKeyName === "" || privateKey === "") { - throw InvalidConfiguration; + throw new InvalidConfiguration("Invalid configuration: privateKey or apiKeyName is empty"); } const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey); const config = new Configuration({ - basePath: BASE_PATH, + basePath: basePath, }); const axiosInstance = globalAxios.create(); axiosInstance.interceptors.request.use(config => - coinbaseAuthenticator.authenticateRequest(config), + coinbaseAuthenticator.authenticateRequest(config, debugging), ); + axiosInstance.interceptors.response.use(response => logApiResponse(response, debugging)); this.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); } @@ -37,37 +44,37 @@ export class Coinbase { * Reads the API key and private key from a JSON file and returns a new instance of 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 {InternalError} If the file does not exist or the configuration values are missing. */ - static fromJsonConfig(filePath: string = "coinbase_cloud_api_key.json"): Coinbase { - /* Read the JSON file for a given path and return a new instance of Coinbase check if the file exists */ + static fromJsonConfig( + filePath: string = "coinbase_cloud_api_key.json", + debugging = false, + basePath: string = BASE_PATH, + ): Coinbase { if (!fs.existsSync(filePath)) { - // throw an error if the file does not exist - throw InvalidConfiguration; + throw new InternalError(`Invalid configuration: file not found at ${filePath}`); } - // read the file and parse the JSON data try { - // read the file and parse the JSON data const data = fs.readFileSync(filePath, "utf8"); - - // parse the JSON data const config = JSON.parse(data); - - // return a new instance of Coinbase if (!config.name || !config.privateKey) { - throw InvalidConfiguration; + throw new InternalError("Invalid configuration: missing configuration values"); } - return new Coinbase(config.name, config.privateKey); + + // Return a new instance of Coinbase + return new Coinbase(config.name, config.privateKey, debugging, basePath); } catch (e) { - throw InvalidConfiguration; + throw new InternalError(`Not able to parse the configuration file`); } } /** * Returns the default user. * @returns {User} The default user. + * @throws {Error} If the user is not found or HTTP request fails. */ async defaultUser(): Promise { const user = await this.apiClients.user?.getCurrentUser(); - return new User(user?.data?.id || "", this.apiClients); + return new User(user?.data?.id as string, this.apiClients); } } diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index 8c72399b..50391ff1 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -1,3 +1,20 @@ -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 { + constructor(message: string = "Invalid API key format") { + super(message); + this.name = "InvalidAPIKeyFormat"; + } +} + +export class InternalError extends Error { + constructor(message: string = "Internal Error") { + super(message); + this.name = "InternalError"; + } +} + +export class InvalidConfiguration extends Error { + constructor(message: string = "Invalid configuration") { + super(message); + this.name = "InvalidConfiguration"; + } +} diff --git a/src/coinbase/tests/authenticator_test.ts b/src/coinbase/tests/authenticator_test.ts index 93f5f7ea..54205a50 100644 --- a/src/coinbase/tests/authenticator_test.ts +++ b/src/coinbase/tests/authenticator_test.ts @@ -1,11 +1,6 @@ import { AxiosHeaders } from "axios"; import { CoinbaseAuthenticator } from "../authenticator"; -const VALID_KEY = - "organizations/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b"; -const VALID_PRIVATE_KEY = - "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n"; - const VALID_CONFIG = { method: "GET", url: "https://api.cdp.coinbase.com/platform/v1/users/me", @@ -13,7 +8,10 @@ const VALID_CONFIG = { }; describe("Authenticator tests", () => { - const authenticator = new CoinbaseAuthenticator(VALID_KEY, VALID_PRIVATE_KEY); + const filePath = "./config/coinbase_cloud_api_key.json"; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const keys = require(filePath); + const authenticator = new CoinbaseAuthenticator(keys.name, keys.privateKey); it("should raise InvalidConfiguration error", async () => { const invalidConfig = { diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 91df8982..d5c1507c 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -23,13 +23,13 @@ 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( - "Invalid configuration", + "Not able to parse the configuration file", ); }); it("should throw an error if the JSON file is not parseable", () => { expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/not_parseable.json`)).toThrow( - "Invalid configuration", + "Not able to parse the configuration file", ); }); @@ -37,7 +37,7 @@ describe("Coinbase tests", () => { axiosMock.onGet().reply(200, { id: 123, }); - const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + const cbInstance = Coinbase.fromJsonConfig(`${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}"); diff --git a/src/coinbase/tests/config/coinbase_cloud_api_key.json b/src/coinbase/tests/config/coinbase_cloud_api_key.json index 9df02493..5ca133fe 100644 --- a/src/coinbase/tests/config/coinbase_cloud_api_key.json +++ b/src/coinbase/tests/config/coinbase_cloud_api_key.json @@ -1,4 +1,4 @@ { - "name": "organizations/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b", + "name": "organizations/ej811111-bf11-4e11-111c-3e3e1e33333b", "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" } \ No newline at end of file diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts new file mode 100644 index 00000000..b1fff913 --- /dev/null +++ b/src/coinbase/tests/user_test.ts @@ -0,0 +1,31 @@ +import { User } from "./../user"; +import { InternalError } from "./../errors"; +import { ApiClients } from "./../types"; + +describe("User", () => { + let mockApiClients: ApiClients; + + beforeEach(() => { + mockApiClients = {} as ApiClients; + }); + + it("should create a User instance with valid userId and client", () => { + const userId = "testUserId"; + const user = new User(userId, mockApiClients); + + expect(user.getUserId()).toBe(userId); + expect(user.toString()).toBe(`Coinbase:User{userId: ${userId}}`); + }); + + it("should throw an InternalError if userId is empty", () => { + expect(() => new User("", mockApiClients)).toThrow(InternalError); + expect(() => new User("", mockApiClients)).toThrow("UserID cannot be empty"); + }); + + it("should throw an InternalError if client is empty", () => { + expect(() => new User("testUserId", null as unknown as ApiClients)).toThrow(InternalError); + expect(() => new User("testUserId", null as unknown as ApiClients)).toThrow( + "Client cannot be empty", + ); + }); +}); diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index 1305467e..e8f0cd51 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -1,8 +1,14 @@ import { AxiosPromise } from "axios"; import { User as UserModel } from "./../client/api"; +/** + * The User API client type definition + */ export type UserAPIClient = { getCurrentUser(options?): AxiosPromise }; +/** + * The API clients type definition for the Coinbase SDK + */ export type ApiClients = { user?: UserAPIClient; }; diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index d5cf2b93..b2e3b497 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -1,10 +1,22 @@ +import { InternalError } from "./errors"; import { ApiClients } from "./types"; +/** + * A representation of a User. + * Users have Wallets, which can hold balances of Assets. + * Access the default User through Coinbase.defaultUser(). + */ export class User { private userId: string = ""; private client: ApiClients; constructor(userId: string, client: ApiClients) { + if (!userId) { + throw new InternalError("UserID cannot be empty"); + } + if (!client) { + throw new InternalError("Client cannot be empty"); + } this.userId = userId; this.client = client; } diff --git a/src/coinbase/utils.ts b/src/coinbase/utils.ts new file mode 100644 index 00000000..bb88e1a0 --- /dev/null +++ b/src/coinbase/utils.ts @@ -0,0 +1,17 @@ +import { AxiosResponse } from "axios"; + +/** + * Prints Axios response to the console for debugging purposes. + * @param response + */ +export const logApiResponse = (response: AxiosResponse, debugging = false): AxiosResponse => { + if (debugging) { + let output = typeof response.data === "string" ? response.data : ""; + + if (typeof response.data === "object") { + output = JSON.stringify(response.data, null, 4); + } + console.log(`API RESPONSE: ${response.status} ${response.config.url} ${output}`); + } + return response; +}; From ea3e0e5557ae78c3c5688923d6fe0e6ce1aac7a8 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 21:04:37 -0500 Subject: [PATCH 05/12] Updating comments --- src/coinbase/authenticator.ts | 25 ++++++++++++------------- src/coinbase/coinbase.ts | 18 +++++++++++------- src/coinbase/types.ts | 4 ++-- src/coinbase/utils.ts | 2 +- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/coinbase/authenticator.ts b/src/coinbase/authenticator.ts index 7affb6e0..e25fb5b6 100644 --- a/src/coinbase/authenticator.ts +++ b/src/coinbase/authenticator.ts @@ -1,6 +1,6 @@ -import { JWK, JWS } from "node-jose"; -import { InternalError, InvalidAPIKeyFormat } from "./errors"; import { InternalAxiosRequestConfig } from "axios"; +import { JWK, JWS } from "node-jose"; +import { InvalidAPIKeyFormat } from "./errors"; const pemHeader = "-----BEGIN EC PRIVATE KEY-----"; const pemFooter = "-----END EC PRIVATE KEY-----"; @@ -22,32 +22,31 @@ export class CoinbaseAuthenticator { } /** - * Middleware to intercept requests and add JWT to the Authorization header for AxiosInterceptor + * 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. - * @throws {InternalError} If there is an issue with the private key. + * @throws {InvalidAPIKeyFormat} If JWT could not be built. */ async authenticateRequest( config: InternalAxiosRequestConfig, - debug = false, + debugging = false, ): Promise { const method = config.method?.toString().toUpperCase(); const token = await this.buildJWT(config.url || "", method); - if (debug) { + if (debugging) { console.log(`API REQUEST: ${method} ${config.url}`); } - config.headers["Authorization"] = `Bearer ${token}`; config.headers["Content-Type"] = "application/json"; return config; } /** - * Builds the JWT for the given API endpoint URI. The JWT is signed with the API key's private key. - * @param {string} url - The URI of the API endpoint. - * @param {string} method - The HTTP method of the request. - * @returns {string} The JWT if successful or throws an error. - * @throws {InvalidAPIKeyFormat} If there is an issue with the private key. + * 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. + * @throws {InvalidAPIKeyFormat} If the private key is not in the correct format. */ async buildJWT(url: string, method = "GET"): Promise { const pemPrivateKey = this.extractPemKey(this.privateKey); @@ -87,7 +86,7 @@ export class CoinbaseAuthenticator { return result as unknown as string; } catch (err) { - throw new InternalError("Could not sign the JWT"); + throw new InvalidAPIKeyFormat("Could not sign the JWT"); } } diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 325a4f55..384d7a8d 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 { InternalError, InvalidConfiguration } from "./errors"; +import { InvalidAPIKeyFormat, InvalidConfiguration } from "./errors"; // The Coinbase SDK. export class Coinbase { @@ -18,6 +18,10 @@ export class Coinbase { * @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 {InvalidConfiguration} If the configuration is invalid. + * @throws {InvalidAPIKeyFormat} If not able to create JWT token. */ constructor( apiKeyName: string, @@ -41,10 +45,10 @@ export class Coinbase { } /** - * Reads the API key and private key from a JSON file and returns a new instance of Coinbase. + * 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 {InternalError} If the file does not exist or the configuration values are missing. + * @throws {InvalidAPIKeyFormat} If the file does not exist or the configuration values are missing/invalid. */ static fromJsonConfig( filePath: string = "coinbase_cloud_api_key.json", @@ -52,24 +56,24 @@ export class Coinbase { basePath: string = BASE_PATH, ): Coinbase { if (!fs.existsSync(filePath)) { - throw new InternalError(`Invalid configuration: file not found at ${filePath}`); + throw new InvalidAPIKeyFormat(`Invalid configuration: file not found at ${filePath}`); } try { const data = fs.readFileSync(filePath, "utf8"); const config = JSON.parse(data); if (!config.name || !config.privateKey) { - throw new InternalError("Invalid configuration: missing configuration values"); + 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 InternalError(`Not able to parse the configuration file`); + throw new InvalidAPIKeyFormat(`Not able to parse the configuration file`); } } /** - * Returns the default user. + * Returns User model for the default user. * @returns {User} The default user. * @throws {Error} If the user is not found or HTTP request fails. */ diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index e8f0cd51..264b079b 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -2,12 +2,12 @@ import { AxiosPromise } from "axios"; import { User as UserModel } from "./../client/api"; /** - * The User API client type definition + * UserAPI client type definition */ export type UserAPIClient = { getCurrentUser(options?): AxiosPromise }; /** - * The API clients type definition for the Coinbase SDK + * API clients type definition for the Coinbase SDK */ export type ApiClients = { user?: UserAPIClient; diff --git a/src/coinbase/utils.ts b/src/coinbase/utils.ts index bb88e1a0..7b3828d9 100644 --- a/src/coinbase/utils.ts +++ b/src/coinbase/utils.ts @@ -2,7 +2,7 @@ import { AxiosResponse } from "axios"; /** * Prints Axios response to the console for debugging purposes. - * @param response + * @param response - The Axios response object. */ export const logApiResponse = (response: AxiosResponse, debugging = false): AxiosResponse => { if (debugging) { From 33fe6769a61f920385c136edfb688b885941296e Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 21:34:10 -0500 Subject: [PATCH 06/12] Updating Model usage in User Object --- src/coinbase/coinbase.ts | 4 ++-- src/coinbase/tests/user_test.ts | 32 ++++++++++++++------------------ src/coinbase/user.ts | 19 ++++++------------- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 384d7a8d..20bec0ef 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -1,6 +1,6 @@ import globalAxios from "axios"; import fs from "fs"; -import { UsersApiFactory } from "../client"; +import { UsersApiFactory, User as UserModel } from "../client"; import { BASE_PATH } from "./../client/base"; import { Configuration } from "./../client/configuration"; import { CoinbaseAuthenticator } from "./authenticator"; @@ -79,6 +79,6 @@ export class Coinbase { */ async defaultUser(): Promise { const user = await this.apiClients.user?.getCurrentUser(); - return new User(user?.data?.id as string, this.apiClients); + return new User(user?.data as UserModel, this.apiClients); } } diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts index b1fff913..0f73523e 100644 --- a/src/coinbase/tests/user_test.ts +++ b/src/coinbase/tests/user_test.ts @@ -1,31 +1,27 @@ import { User } from "./../user"; -import { InternalError } from "./../errors"; import { ApiClients } from "./../types"; +import { User as UserModel } from "./../../client/api"; -describe("User", () => { +describe("User Class", () => { + let mockUserModel: UserModel; let mockApiClients: ApiClients; beforeEach(() => { - mockApiClients = {} as ApiClients; - }); + mockUserModel = { + id: "12345", + } as UserModel; - it("should create a User instance with valid userId and client", () => { - const userId = "testUserId"; - const user = new User(userId, mockApiClients); - - expect(user.getUserId()).toBe(userId); - expect(user.toString()).toBe(`Coinbase:User{userId: ${userId}}`); + mockApiClients = {} as ApiClients; }); - it("should throw an InternalError if userId is empty", () => { - expect(() => new User("", mockApiClients)).toThrow(InternalError); - expect(() => new User("", mockApiClients)).toThrow("UserID cannot be empty"); + it("should correctly initialize with given user model and api clients with a valid user id", () => { + const user = new User(mockUserModel, mockApiClients); + expect(user).toBeInstanceOf(User); + expect(user.getUserId()).toBe(mockUserModel.id); }); - it("should throw an InternalError if client is empty", () => { - expect(() => new User("testUserId", null as unknown as ApiClients)).toThrow(InternalError); - expect(() => new User("testUserId", null as unknown as ApiClients)).toThrow( - "Client cannot be empty", - ); + it("should return the correct string representation", () => { + const user = new User(mockUserModel, mockApiClients); + expect(user.toString()).toBe(`Coinbase:User{userId: ${mockUserModel.id}}`); }); }); diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index b2e3b497..7d7a7581 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -1,31 +1,24 @@ -import { InternalError } from "./errors"; import { ApiClients } from "./types"; - +import { User as UserModel } from "./../client/api"; /** * A representation of a User. * Users have Wallets, which can hold balances of Assets. * Access the default User through Coinbase.defaultUser(). */ export class User { - private userId: string = ""; + private model: UserModel; private client: ApiClients; - constructor(userId: string, client: ApiClients) { - if (!userId) { - throw new InternalError("UserID cannot be empty"); - } - if (!client) { - throw new InternalError("Client cannot be empty"); - } - this.userId = userId; + constructor(user: UserModel, client: ApiClients) { this.client = client; + this.model = user; } public getUserId(): string { - return this.userId; + return this.model.id; } toString(): string { - return `Coinbase:User{userId: ${this.userId}}`; + return `Coinbase:User{userId: ${this.model.id}}`; } } From 15fa64f9567df9e9e404e0318ad905968c556e0f Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 21:37:24 -0500 Subject: [PATCH 07/12] Adding JSDocs for types file --- src/coinbase/tests/user_test.ts | 4 ++-- src/coinbase/types.ts | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts index 0f73523e..bc777090 100644 --- a/src/coinbase/tests/user_test.ts +++ b/src/coinbase/tests/user_test.ts @@ -14,13 +14,13 @@ describe("User Class", () => { mockApiClients = {} as ApiClients; }); - it("should correctly initialize with given user model and api clients with a valid user id", () => { + 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 the correct string representation", () => { + 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}}`); }); diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index 264b079b..bdb22619 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -2,13 +2,28 @@ import { AxiosPromise } from "axios"; import { User as UserModel } from "./../client/api"; /** - * UserAPI client type definition + * UserAPI client type definition. */ -export type UserAPIClient = { getCurrentUser(options?): AxiosPromise }; +export type UserAPIClient = { + /** + * Retrieves the current user. + * @param {AxiosRequestConfig} [options] - Axios request options. + * @returns {AxiosPromise} - A promise resolving to the User model. + * @throws {Error} If the request fails. + */ + getCurrentUser(options?): AxiosPromise; +}; /** - * API clients type definition for the Coinbase SDK + * 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 = { + /** + * The User API client. + * @type {UserAPIClient} + */ user?: UserAPIClient; }; From 6d43e5459eed3d9e52879bf249332494a15b9d48 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 22:42:10 -0500 Subject: [PATCH 08/12] 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 | 22 +++++++++------- src/coinbase/types.ts | 6 ++--- src/coinbase/utils.ts | 7 ++++- 7 files changed, 69 insertions(+), 39 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..1d515a46 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,7 +39,7 @@ describe("Coinbase tests", () => { ); }); - it("should able to get the default user", async () => { + it("should be able to get the default user", async () => { axiosMock.onGet().reply(200, { id: 123, }); @@ -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().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; }; From 243cd630a749f340bbbd6855c26389b38cc8c6f4 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 23:05:56 -0500 Subject: [PATCH 09/12] Renaming fromJsonConfig method --- src/coinbase/coinbase.ts | 2 +- src/coinbase/tests/coinbase_test.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 71271bb1..38510778 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -52,7 +52,7 @@ export class Coinbase { * @throws {InvalidConfiguration} If the configuration is invalid. * @throws {InvalidAPIKeyFormat} If not able to create JWT token. */ - static fromJsonConfig( + static configureFromJson( filePath: string = "coinbase_cloud_api_key.json", debugging: boolean = false, basePath: string = BASE_PATH, diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 1d515a46..03052963 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -17,24 +17,24 @@ describe("Coinbase tests", () => { }); it("should throw an error if the file does not exist", () => { - expect(() => Coinbase.fromJsonConfig(`${PATH_PREFIX}/does-not-exist.json`)).toThrow( + 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.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + 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.fromJsonConfig(`${PATH_PREFIX}/invalid.json`)).toThrow( + 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.fromJsonConfig(`${PATH_PREFIX}/not_parseable.json`)).toThrow( + expect(() => Coinbase.configureFromJson(`${PATH_PREFIX}/not_parseable.json`)).toThrow( "Not able to parse the configuration file", ); }); @@ -43,7 +43,10 @@ describe("Coinbase tests", () => { axiosMock.onGet().reply(200, { id: 123, }); - const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`, true); + 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}"); @@ -51,7 +54,7 @@ describe("Coinbase tests", () => { it("should raise an error if the user is not found", async () => { axiosMock.onGet().reply(404); - const cbInstance = Coinbase.fromJsonConfig(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); await expect(cbInstance.defaultUser()).rejects.toThrow("Request failed with status code 404"); }); }); From 75e931869aedf0534d1760bd4e93971bd5e6a36a Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 23:09:26 -0500 Subject: [PATCH 10/12] updating throw message --- src/coinbase/tests/coinbase_test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 03052963..09cd27ee 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -55,6 +55,8 @@ describe("Coinbase tests", () => { 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("Request failed with status code 404"); + await expect(cbInstance.defaultUser()).rejects.toThrow( + "Failed to retrieve user: Request failed with status code 404", + ); }); }); From e3693c62d82924b4fdecddc8d81c6660cabc871b Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 14 May 2024 11:37:23 -0500 Subject: [PATCH 11/12] Missing docs and updating method names --- src/coinbase/coinbase.ts | 9 ++++++--- src/coinbase/errors.ts | 10 ++++++++++ src/coinbase/tests/coinbase_test.ts | 11 +++++------ src/coinbase/tests/user_test.ts | 2 +- src/coinbase/user.ts | 15 ++++++++++++++- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 38510778..000275d0 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -29,8 +29,11 @@ export class Coinbase { debugging = false, basePath: string = BASE_PATH, ) { - if (apiKeyName === "" || privateKey === "") { - throw new InternalError("Invalid configuration: privateKey or apiKeyName is empty"); + if (apiKeyName === "") { + throw new InternalError("Invalid configuration: apiKeyName is empty"); + } + if (privateKey === "") { + throw new InternalError("Invalid configuration: privateKey is empty"); } const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey); const config = new Configuration({ @@ -84,7 +87,7 @@ export class Coinbase { * @returns {User} The default user. * @throws {InternalError} If the request fails. */ - async defaultUser(): Promise { + async getDefaultUser(): Promise { try { const userResponse = await this.apiClients.user!.getCurrentUser(); return new User(userResponse.data as UserModel, this.apiClients); diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index f95fa262..c75eb6ea 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -1,3 +1,7 @@ +/** + * InvalidaAPIKeyFormat error is thrown when the API key format is invalid. + * @extends {Error} + */ export class InvalidAPIKeyFormat extends Error { static DEFAULT_MESSAGE = "Invalid API key format"; @@ -10,6 +14,9 @@ export class InvalidAPIKeyFormat extends Error { } } +/** + * InternalError is thrown when there is an internal error in the SDK. + */ export class InternalError extends Error { static DEFAULT_MESSAGE = "Internal Error"; @@ -22,6 +29,9 @@ export class InternalError extends Error { } } +/** + * InvalidConfiguration error is thrown when apikey/privateKey configuration is invalid. + */ export class InvalidConfiguration extends Error { static DEFAULT_MESSAGE = "Invalid configuration"; diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 09cd27ee..ca5cb423 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -11,9 +11,8 @@ describe("Coinbase tests", () => { }); 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", - ); + expect(() => new Coinbase("", "test")).toThrow("Invalid configuration: apiKeyName is empty"); + expect(() => new Coinbase("test", "")).toThrow("Invalid configuration: privateKey is empty"); }); it("should throw an error if the file does not exist", () => { @@ -47,15 +46,15 @@ describe("Coinbase tests", () => { `${PATH_PREFIX}/coinbase_cloud_api_key.json`, true, ); - const user = await cbInstance.defaultUser(); - expect(user.getUserId()).toBe(123); + const user = await cbInstance.getDefaultUser(); + expect(user.getId()).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( + await expect(cbInstance.getDefaultUser()).rejects.toThrow( "Failed to retrieve user: Request failed with status code 404", ); }); diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts index bc777090..ce047872 100644 --- a/src/coinbase/tests/user_test.ts +++ b/src/coinbase/tests/user_test.ts @@ -17,7 +17,7 @@ describe("User Class", () => { 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); + expect(user.getId()).toBe(mockUserModel.id); }); it("should return a correctly formatted string representation of the User instance", () => { diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index 7d7a7581..59d063f1 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -9,15 +9,28 @@ export class User { private model: UserModel; private client: ApiClients; + /** + * Initializes a new User instance. + * @param {UserModel} user - The user model. + * @param {ApiClients} client - The API clients. + */ constructor(user: UserModel, client: ApiClients) { this.client = client; this.model = user; } - public getUserId(): string { + /** + * Returns the user's ID. + * @returns {string} The user's ID. + */ + public getId(): string { return this.model.id; } + /** + * Returns a string representation of the User. + * @returns {string} The string representation of the User. + */ toString(): string { return `Coinbase:User{userId: ${this.model.id}}`; } From d6f6b1f56fc78803c826c8623ae4925f43597a8c Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 14 May 2024 11:46:12 -0500 Subject: [PATCH 12/12] Updating user jsdoc --- src/coinbase/coinbase.ts | 2 +- src/coinbase/errors.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 000275d0..ea80bc43 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -83,7 +83,7 @@ export class Coinbase { } /** - * Returns User model for the default user. + * Returns User object for the default user. * @returns {User} The default user. * @throws {InternalError} If the request fails. */ diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index c75eb6ea..94d50f0a 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -5,6 +5,10 @@ export class InvalidAPIKeyFormat extends Error { static DEFAULT_MESSAGE = "Invalid API key format"; + /** + * Initializes a new InvalidAPIKeyFormat instance. + * @param message - The error message. + */ constructor(message: string = InvalidAPIKeyFormat.DEFAULT_MESSAGE) { super(message); this.name = "InvalidAPIKeyFormat"; @@ -20,6 +24,10 @@ export class InvalidAPIKeyFormat extends Error { export class InternalError extends Error { static DEFAULT_MESSAGE = "Internal Error"; + /** + * Initializes a new InternalError instance. + * @param message - The error message. + */ constructor(message: string = InternalError.DEFAULT_MESSAGE) { super(message); this.name = "InternalError"; @@ -35,6 +43,10 @@ export class InternalError extends Error { export class InvalidConfiguration extends Error { static DEFAULT_MESSAGE = "Invalid configuration"; + /** + * Initializes a new InvalidConfiguration instance. + * @param message - The error message. + */ constructor(message: string = InvalidConfiguration.DEFAULT_MESSAGE) { super(message); this.name = "InvalidConfiguration";