Skip to content
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

Merged
merged 12 commits into from
May 14, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions src/coinbase/coinbase.ts
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

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

Copy link
Contributor Author

@erdimaden erdimaden May 14, 2024

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

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 === "") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets explicitly say which one is empty?

throw InvalidConfiguration;
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
}
const coinbaseAuthenticator = new CoinbaseAuthenticator(apiKeyName, privateKey);
const config = new Configuration({
basePath: BASE_PATH,
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
});
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
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
throw InvalidConfiguration;
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
}
// 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;
}
return new Coinbase(config.name, config.privateKey);
} catch (e) {
throw InvalidConfiguration;
}
}

/**
* Returns the default user.
* @returns {User} The default user.
*/
async defaultUser(): Promise<User> {
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async defaultUser(): Promise<User> {
async getDefaultUser(): Promise<User> {

const user = await this.apiClients.user?.getCurrentUser();
return new User(user?.data?.id || "", this.apiClients);
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
}
}
53 changes: 53 additions & 0 deletions src/coinbase/tests/coinbase_test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
4 changes: 4 additions & 0 deletions src/coinbase/tests/config/coinbase_cloud_api_key.json
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/0c3bbe72-ac81-46ec-946a-7cd019d6d86b/apiKeys/db813705-bf33-4e33-816c-4c3f1f54672b",
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
"privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n"
}
4 changes: 4 additions & 0 deletions src/coinbase/tests/config/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": 0,
"apiSecret": ""
}
1 change: 1 addition & 0 deletions src/coinbase/tests/config/not_parseable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
not parseable content
8 changes: 8 additions & 0 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AxiosPromise } from "axios";
import { User as UserModel } from "./../client/api";

export type UserAPIClient = { getCurrentUser(options?): AxiosPromise<UserModel> };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSdoc


export type ApiClients = {
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
user?: UserAPIClient;
};
19 changes: 19 additions & 0 deletions src/coinbase/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ApiClients } from "./types";

export class User {
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
private userId: string = "";
private client: ApiClients;

constructor(userId: string, client: ApiClients) {
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
this.userId = userId;
this.client = client;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should (User, Wallet, Address etc..) classes create member vars for each of their respective fields or should the class encapsulate the generated model (UserModel) and use the model to retrieve field values in the getters?

The diff would be roughly:

Suggested change
export class User {
private userId: string = "";
private client: ApiClients;
constructor(userId: string, client: ApiClients) {
this.userId = userId;
this.client = client;
}
import { User as UserModel } from "../clients/api");
export class User {
private model: UserModel;
private client: ApiClients;
constructor(model: UserModel, client: ApiClients) {
this.model = model;
this.client = client;
}
public getUserId(): string {
return this.model.user_id:
}

This seems a bit cleaner especially for classes such as Address and Transfer where we would be duplicating / redeclaring lots of fields otherwise and is more similar to what we do with the Ruby SDK.

Curious everyone's thoughts here! @erdimaden @yuga-cb @alex-stone

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I appreciate the use of a generated model, I personally lean towards including fields within the same class. This approach can help reduce dependency on the generated code and provide more control over modifications that might arise from changes in the generated code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not deviate approaches between SDKs. Please use and delegate to the generated model. If we don't think that is the right solution, then we should evaluate that holistically for both the JS and Ruby SDK.


public getUserId(): string {
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typedoc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public getUserId(): string {
public getId(): string {

return this.userId;
}

toString(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typedoc

return `Coinbase:User{userId: ${this.userId}}`;
}
}
65 changes: 64 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -2477,6 +2505,11 @@ [email protected]:
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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading