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

feat: Adding Address Class with tests #10

Merged
merged 7 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/coinbase/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AxiosError } from "axios";
import { Address as AddressModel } from "../client";
import { InternalError } from "./errors";
import { FaucetTransaction } from "./faucet_transaction";
import { AddressAPIClient } from "./types";

/**
* A representation of a blockchain address, which is a user-controlled account on a network.
*/
export class Address {
private model: AddressModel;
private client: AddressAPIClient;

/**
* Initializes a new Address instance.
* @param {AddressModel} model - The address model data.
* @param {AddressAPIClient} client - The API client to interact with address-related endpoints.
* @throws {InternalError} If the model or client is empty.
*/
constructor(model: AddressModel, client: AddressAPIClient) {
if (!model) {
throw new InternalError("Address model cannot be empty");
}
if (!client) {
throw new InternalError("Address client cannot be empty");
}
this.model = model;
this.client = client;
}

/**
* Requests faucet funds for the address.
Copy link
Contributor

Choose a reason for hiding this comment

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

note: only supported on testnet networks

* Only supported on testnet networks.
* @returns {Promise<FaucetTransaction>} The faucet transaction object.
* @throws {InternalError} If the request does not return a transaction hash.
* @throws {Error} If the request fails.
*/
async faucet(): Promise<FaucetTransaction> {
try {
const response = await this.client.requestFaucetFunds(
this.model.wallet_id,
this.model.address_id,
);
return new FaucetTransaction(response.data);
} catch (e) {
if (e instanceof AxiosError) {
throw e;
}
throw new Error(`Failed to complete faucet request`);
}
}

/**
* Returns the address ID.
* @returns {string} The address ID.
*/
public getId(): string {
return this.model.address_id;
}

/**
* Returns the network ID.
* @returns {string} The network ID.
*/
public getNetworkId(): string {
return this.model.network_id;
}

/**
* Returns the public key.
* @returns {string} The public key.
*/
public getPublicKey(): string {
return this.model.public_key;
}

/**
* Returns the wallet ID.
* @returns {string} The wallet ID.
*/
public getWalletId(): string {
return this.model.wallet_id;
}

/**
* Returns a string representation of the address.
* @returns {string} A string representing the address.
*/
public toString(): string {
return `Coinbase:Address{addressId: '${this.model.address_id}', networkId: '${this.model.network_id}', walletId: '${this.model.wallet_id}'}`;
}
}
2 changes: 1 addition & 1 deletion src/coinbase/faucet_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class FaucetTransaction {
* @throws {InternalError} If the model does not exist.
*/
constructor(model: FaucetTransactionModel) {
if (!model) {
if (!model?.transaction_hash) {
throw new InternalError("FaucetTransaction model cannot be empty");
}
this.model = model;
Expand Down
74 changes: 74 additions & 0 deletions src/coinbase/tests/address_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ethers } from "ethers";
import { AddressesApiFactory, Address as AddressModel } from "../../client";
import { Address } from "./../address";
import { FaucetTransaction } from "./../faucet_transaction";

import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { randomUUID } from "crypto";

const axiosMock = new MockAdapter(axios);

const newEthAddress = ethers.Wallet.createRandom();

const VALID_ADDRESS_MODEL: AddressModel = {
address_id: newEthAddress.address,
network_id: "SEPOLIA",
erdimaden marked this conversation as resolved.
Show resolved Hide resolved
public_key: newEthAddress.publicKey,
wallet_id: randomUUID(),
};

// Test suite for Address class
describe("Address", () => {
const client = AddressesApiFactory();

afterEach(() => {
axiosMock.reset();
});

it("should create an Address instance", () => {
const address = new Address(VALID_ADDRESS_MODEL, client);
expect(address).toBeInstanceOf(Address);
expect(address.getId()).toBe(newEthAddress.address);
expect(address.getPublicKey()).toBe(newEthAddress.publicKey);
expect(address.getNetworkId()).toBe(VALID_ADDRESS_MODEL.network_id);
expect(address.getWalletId()).toBe(VALID_ADDRESS_MODEL.wallet_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Separate describe block tests for the getter methods?

Ruby SDK Reference

});

it("should throw an InternalError if model is not provided", () => {
expect(() => new Address(null!, client)).toThrow(`Address model cannot be empty`);
});

it("should throw an InternalError if client is not provided", () => {
expect(() => new Address(VALID_ADDRESS_MODEL, null!)).toThrow(`Address client cannot be empty`);
});

it("should request faucet funds and return a FaucetTransaction", async () => {
axiosMock.onPost().reply(200, {
transaction_hash: "mocked_transaction_hash",
});
const address = new Address(VALID_ADDRESS_MODEL, client);
const faucetTransaction = await address.faucet();
expect(faucetTransaction).toBeInstanceOf(FaucetTransaction);
expect(faucetTransaction.getTransactionHash()).toBe("mocked_transaction_hash");
});

it("should request faucet funds and throw an InternalError if the request does not return a transaction hash", async () => {
axiosMock.onPost().reply(200, {});
const address = new Address(VALID_ADDRESS_MODEL, client);
Copy link
Contributor

Choose a reason for hiding this comment

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

these can be moved to beforeEach block

await expect(address.faucet()).rejects.toThrow("Failed to complete faucet request");
});

it("should throw an AxiosError if faucet request fails", async () => {
axiosMock.onPost().reply(400);
const address = new Address(VALID_ADDRESS_MODEL, client);
await expect(address.faucet()).rejects.toThrow("Request failed with status code 400");
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets group these in a describe for the method. There also is a few things missing for Ruby SDK parity in these tests.

  1. Faucet limit reached failure case
  2. let(:tx_hash) { '0xdeadbeef' } - value for transaction hash response is different

reference


it("should return the correct string representation", () => {
const address = new Address(VALID_ADDRESS_MODEL, client);
expect(address.toString()).toBe(
`Coinbase:Address{addressId: '${VALID_ADDRESS_MODEL.address_id}', networkId: '${VALID_ADDRESS_MODEL.network_id}', walletId: '${VALID_ADDRESS_MODEL.wallet_id}'}`,
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { InternalError } from "./../errors";
import { FaucetTransaction } from "../faucet_transaction";

describe("FaucetTransaction tests", () => {
Expand Down
19 changes: 18 additions & 1 deletion src/coinbase/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { AxiosPromise, AxiosRequestConfig } from "axios";
import { User as UserModel } from "./../client/api";

/**
* AddressAPI client type definition.
*/
export type AddressAPIClient = {
/**
* Requests faucet funds for the address.
* @param {string} walletId - The wallet ID.
* @param {string} addressId - The address ID.
* @returns {Promise<{ data: { transaction_hash: string } }>} - The transaction hash
* @throws {AxiosError} If the request fails.
*/
requestFaucetFunds(
walletId: string,
addressId: string,
): Promise<{ data: { transaction_hash: string } }>;
};

/**
* UserAPI client type definition.
*/
Expand All @@ -9,7 +26,7 @@ export type UserAPIClient = {
* Retrieves the current user.
* @param {AxiosRequestConfig} [options] - Axios request options.
* @returns {AxiosPromise<UserModel>} - A promise resolving to the User model.
* @throws {Error} If the request fails.
* @throws {AxiosError} If the request fails.
*/
getCurrentUser(options?: AxiosRequestConfig): AxiosPromise<UserModel>;
};
Expand Down
Loading