From 623997ab157be0509e4e466bc7f68a5b6315093f Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 13 May 2024 17:00:24 -0500 Subject: [PATCH] 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"