Skip to content

Commit

Permalink
Adding listBalance, getBalance, getAddress and canSign methods
Browse files Browse the repository at this point in the history
  • Loading branch information
erdimaden committed May 22, 2024
1 parent 055c8dd commit aabf433
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 4 deletions.
7 changes: 7 additions & 0 deletions src/coinbase/coinbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export class Coinbase {
*/
static readonly WEI_PER_ETHER: bigint = BigInt("1000000000000000000");

/**
* Represents the number of Gwei per Ether.
*
* @constant
*/
static readonly GWEI_PER_ETHER: bigint = BigInt("1000000000");

/**
* The backup file path for Wallet seeds.
*
Expand Down
4 changes: 2 additions & 2 deletions src/coinbase/tests/balance_map_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("BalanceMap", () => {
});
});

describe("#add", () => {
describe(".add", () => {
const assetId = Coinbase.assetList.Eth;
const balance = Balance.fromModelAndAssetId(
{
Expand All @@ -66,7 +66,7 @@ describe("BalanceMap", () => {
});
});

describe("#toString", () => {
describe(".toString", () => {
const assetId = Coinbase.assetList.Eth;
const balance = Balance.fromModelAndAssetId(
{
Expand Down
2 changes: 2 additions & 0 deletions src/coinbase/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export const usersApiMock = {
export const walletsApiMock = {
getWallet: jest.fn(),
createWallet: jest.fn(),
listWalletBalances: jest.fn(),
getWalletBalance: jest.fn(),
};

export const addressesApiMock = {
Expand Down
169 changes: 167 additions & 2 deletions src/coinbase/tests/wallet_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { randomUUID } from "crypto";
import { Coinbase } from "../coinbase";
import { Wallet } from "../wallet";
import { Address as AddressModel, Wallet as WalletModel } from "./../../client";
import { addressesApiMock, mockFn, newAddressModel, walletsApiMock } from "./utils";
import {
AddressBalanceList,
Address as AddressModel,
Wallet as WalletModel,
Balance as BalanceModel,
} from "./../../client";
import { ArgumentError } from "../errors";
import {
addressesApiMock,
mockFn,
mockReturnValue,
newAddressModel,
walletsApiMock,
} from "./utils";
import { Address } from "../address";
import Decimal from "decimal.js";

describe("Wallet Class", () => {
let wallet, walletModel, walletId;
Expand Down Expand Up @@ -171,4 +184,156 @@ describe("Wallet Class", () => {
expect(newWallet).toBeInstanceOf(Wallet);
});
});

describe(".defaultAddress", () => {
let wallet, walletId;
beforeEach(async () => {
jest.clearAllMocks();
walletId = randomUUID();
const mockAddressModel: AddressModel = newAddressModel(walletId);
const walletMockWithDefaultAddress: WalletModel = {
id: walletId,
network_id: Coinbase.networkList.BaseSepolia,
default_address: mockAddressModel,
};
Coinbase.apiClients.wallet = walletsApiMock;
Coinbase.apiClients.address = addressesApiMock;
Coinbase.apiClients.wallet!.createWallet = mockReturnValue(walletMockWithDefaultAddress);
Coinbase.apiClients.wallet!.getWallet = mockReturnValue(walletMockWithDefaultAddress);
Coinbase.apiClients.address!.createAddress = mockReturnValue(mockAddressModel);
wallet = await Wallet.create();
});

it("should return the correct address", async () => {
const defaultAddress = wallet.defaultAddress();
const address = wallet.getAddress(defaultAddress?.getId());
expect(address).toBeInstanceOf(Address);
expect(address?.getId()).toBe(address.getId());
});

describe(".walletId", () => {
it("should return the correct wallet ID", async () => {
expect(wallet.getId()).toBe(walletId);
});
});

describe(".networkId", () => {
it("should return the correct network ID", async () => {
expect(wallet.getNetworkId()).toBe(Coinbase.networkList.BaseSepolia);
});
});
});

describe(".listBalances", () => {
beforeEach(() => {
const mockListBalanceResponse: AddressBalanceList = {
data: [
{
amount: "1000000000000000000",
asset: {
asset_id: Coinbase.assetList.Eth,
network_id: Coinbase.networkList.BaseSepolia,
decimals: 18,
},
},
{
amount: "5000000",
asset: {
asset_id: "usdc",
network_id: Coinbase.networkList.BaseSepolia,
decimals: 6,
},
},
],
has_more: false,
next_page: "",
total_count: 2,
};
Coinbase.apiClients.wallet!.listWalletBalances = mockReturnValue(mockListBalanceResponse);
});

it("should return a hash with an ETH and USDC balance", async () => {
const balanceMap = await wallet.listBalances();
expect(balanceMap.get("eth")).toEqual(new Decimal(1));
expect(balanceMap.get("usdc")).toEqual(new Decimal(5));
expect(Coinbase.apiClients.wallet!.listWalletBalances).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.wallet!.listWalletBalances).toHaveBeenCalledWith(walletId);
});
});

describe(".getBalance", () => {
beforeEach(() => {
const response: BalanceModel = {
amount: "5000000000000000000",
asset: {
asset_id: Coinbase.assetList.Eth,
network_id: Coinbase.networkList.BaseSepolia,
decimals: 18,
},
};
Coinbase.apiClients.wallet!.getWalletBalance = mockReturnValue(response);
});

it("should return the correct ETH balance", async () => {
const balanceMap = await wallet.getBalance(Coinbase.assetList.Eth);
expect(balanceMap).toEqual(new Decimal(5));
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledWith(
walletId,
Coinbase.assetList.Eth,
);
});

it("should return the correct GWEI balance", async () => {
const balance = await wallet.getBalance(Coinbase.assetList.Gwei);
expect(balance).toEqual(new Decimal((BigInt(5) * Coinbase.GWEI_PER_ETHER).toString()));
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledWith(
walletId,
Coinbase.assetList.Gwei,
);
});

it("should return the correct WEI balance", async () => {
const balance = await wallet.getBalance(Coinbase.assetList.Wei);
expect(balance).toEqual(new Decimal((BigInt(5) * Coinbase.WEI_PER_ETHER).toString()));
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledWith(
walletId,
Coinbase.assetList.Wei,
);
});

it("should return 0 when the balance is not found", async () => {
Coinbase.apiClients.wallet!.getWalletBalance = mockReturnValue({});
const balance = await wallet.getBalance(Coinbase.assetList.Wei);
expect(balance).toEqual(new Decimal(0));
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.wallet!.getWalletBalance).toHaveBeenCalledWith(
walletId,
Coinbase.assetList.Wei,
);
});
});

describe(".canSign", () => {
let wallet;
beforeAll(async () => {
const mockAddressModel = newAddressModel(walletId);
const mockWalletModel = {
id: walletId,
network_id: Coinbase.networkList.BaseSepolia,
default_address: mockAddressModel,
};
Coinbase.apiClients.wallet = walletsApiMock;
Coinbase.apiClients.address = addressesApiMock;
Coinbase.apiClients.wallet!.createWallet = mockReturnValue(mockWalletModel);
Coinbase.apiClients.wallet!.getWallet = mockReturnValue(mockWalletModel);
Coinbase.apiClients.address!.createAddress = mockReturnValue(mockAddressModel);
wallet = await Wallet.create();
});
it("should return true when the wallet initialized ", () => {
expect(wallet.canSign()).toBe(true);
});
});
});
35 changes: 35 additions & 0 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ export type WalletAPIClient = {
* @throws {APIError} If the request fails.
*/
getWallet: (walletId: string, options?: RawAxiosRequestConfig) => AxiosPromise<WalletModel>;

/**
* List the balances of all of the addresses in the wallet aggregated by asset.
*
* @param walletId - The ID of the wallet to fetch the balances for
* @param options - Override http request option.
* @throws {RequiredError} If the required parameter is not provided.
* @throws {APIError} If the request fails.
*/
listWalletBalances(walletId: string, options?): AxiosPromise<AddressBalanceList>;

/**
* List the balances of all of the addresses in the wallet aggregated by asset.
*
* @param walletId - The ID of the wallet to fetch the balances for
* @param options - Override http request option.
* @throws {RequiredError} If the required parameter is not provided.
* @throws {APIError} If the request fails.
*/
listWalletBalances(walletId: string, options?): AxiosPromise<AddressBalanceList>;

/**
* Get the aggregated balance of an asset across all of the addresses in the wallet.
*
* @param walletId - The ID of the wallet to fetch the balance for
* @param assetId - The symbol of the asset to fetch the balance for
* @param options - Override http request option.
* @throws {RequiredError} If the required parameter is not provided.
* @throws {APIError} If the request fails.
*/
getWalletBalance(
walletId: string,
assetId: string,
options?: RawAxiosRequestConfig,
): AxiosPromise<Balance>;
};

/**
Expand Down
49 changes: 49 additions & 0 deletions src/coinbase/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { ArgumentError, InternalError } from "./errors";
import { FaucetTransaction } from "./faucet_transaction";
import { WalletData } from "./types";
import { convertStringToHex } from "./utils";
import { BalanceMap } from "./balance_map";
import Decimal from "decimal.js";
import { Balance } from "./balance";

/**
* A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses,
Expand Down Expand Up @@ -207,6 +210,43 @@ export class Wallet {
this.addressIndex++;
}

/**
* Returns the Address with the given ID.
*
* @param addressId - The ID of the Address to retrieve.
* @returns The Address.
*/
public getAddress(addressId: string): Address | undefined {
return this.addresses.find(address => {
return address.getId() === addressId;
});
}

/**
* Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
*
* @returns The list of balances. The key is the Asset ID, and the value is the balance.
*/
public async listBalances(): Promise<BalanceMap> {
const response = await Coinbase.apiClients.wallet!.listWalletBalances(this.model.id!);
return BalanceMap.fromBalances(response.data.data);
}

/**
* Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.
*
* @param assetId - The ID of the Asset to retrieve the balance for.
* @returns The balance of the Asset.
*/
public async getBalance(assetId: string): Promise<Decimal> {
const response = await Coinbase.apiClients.wallet!.getWalletBalance(this.model.id!, assetId);
if (!response.data.amount) {
return new Decimal(0);
}
const balance = Balance.fromModelAndAssetId(response.data, assetId);
return balance.amount;
}

/**
* Returns the Network ID of the Wallet.
*
Expand Down Expand Up @@ -234,6 +274,15 @@ export class Wallet {
return this.model.default_address ? new Address(this.model.default_address) : undefined;
}

/**
* Returns whether the Wallet has a seed with which to derive keys and sign transactions.
*
* @returns Whether the Wallet has a seed with which to derive keys and sign transactions.
*/
public canSign(): boolean {
return !!this.master;
}

/**
* Requests funds from the faucet for the Wallet's default address and returns the faucet transaction.
* This is only supported on testnet networks.
Expand Down

0 comments on commit aabf433

Please sign in to comment.