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

Implementing getWallet functionality #27

Merged
merged 6 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
117 changes: 98 additions & 19 deletions src/coinbase/tests/user_test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import * as fs from "fs";
import * as crypto from "crypto";
import * as bip39 from "bip39";
import * as crypto from "crypto";
import * as fs from "fs";
import { ArgumentError } from "../errors";
import {
AddressList,
Address as AddressModel,
User as UserModel,
Wallet as WalletModel,
Address as AddressModel,
AddressList,
} from "./../../client/api";
import { User } from "./../user";
import { Coinbase } from "./../coinbase";
import { SeedData, WalletData } from "./../types";
import { User } from "./../user";
import { Wallet } from "./../wallet";
import {
mockFn,
walletsApiMock,
addressesApiMock,
newAddressModel,
mockReturnRejectedValue,
mockReturnValue,
newAddressModel,
walletsApiMock,
} from "./utils";
import { SeedData, WalletData } from "./../types";
import { Wallet } from "./../wallet";
import { ArgumentError } from "../errors";

describe("User Class", () => {
let mockUserModel: UserModel;
Expand Down Expand Up @@ -67,7 +67,6 @@ describe("User Class", () => {
Coinbase.apiClients.wallet!.getWallet = mockReturnValue(mockWalletModel);
Coinbase.apiClients.address = addressesApiMock;
Coinbase.apiClients.address!.listAddresses = mockReturnValue(mockAddressList);
Coinbase.apiClients.address!.getAddress = mockReturnValue(mockAddressModel);
user = new User(mockUserModel);
walletData = { walletId: walletId, seed: bip39.generateMnemonic() };
importedWallet = await user.importWallet(walletData);
Expand All @@ -76,7 +75,6 @@ describe("User Class", () => {
expect(Coinbase.apiClients.wallet!.getWallet).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledWith(walletId);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(1);
});

it("should import an exported wallet", async () => {
Expand All @@ -94,15 +92,13 @@ describe("User Class", () => {

describe(".saveWallet", () => {
let seed: string;
let addressCount: number;
let walletId: string;
let mockSeedWallet: Wallet;
let savedWallet: Wallet;

beforeAll(async () => {
walletId = crypto.randomUUID();
seed = "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe";
addressCount = 1;
mockAddressModel = {
address_id: "0xdeadbeef",
wallet_id: walletId,
Expand All @@ -123,7 +119,7 @@ describe("User Class", () => {
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
}).privateKey;
mockSeedWallet = await Wallet.init(mockWalletModel, seed, addressCount);
mockSeedWallet = await Wallet.init(mockWalletModel, seed, [mockAddressModel]);
});

afterEach(async () => {
Expand All @@ -133,7 +129,6 @@ describe("User Class", () => {
it("should save the Wallet data when encryption is false", async () => {
savedWallet = user.saveWallet(mockSeedWallet);
expect(savedWallet).toBe(mockSeedWallet);
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(1);
const storedSeedData = fs.readFileSync(Coinbase.backupFilePath);
const walletSeedData = JSON.parse(storedSeedData.toString());
expect(walletSeedData[walletId].encrypted).toBe(false);
Expand All @@ -145,7 +140,6 @@ describe("User Class", () => {
it("should save the Wallet data when encryption is true", async () => {
savedWallet = user.saveWallet(mockSeedWallet, true);
expect(savedWallet).toBe(mockSeedWallet);
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(1);
const storedSeedData = fs.readFileSync(Coinbase.backupFilePath);
const walletSeedData = JSON.parse(storedSeedData.toString());
expect(walletSeedData[walletId].encrypted).toBe(true);
Expand Down Expand Up @@ -254,7 +248,6 @@ describe("User Class", () => {
Coinbase.apiClients.wallet!.getWallet = mockReturnValue(walletModelWithDefaultAddress);
Coinbase.apiClients.address = addressesApiMock;
Coinbase.apiClients.address!.listAddresses = mockReturnValue(addressListModel);
Coinbase.apiClients.address!.getAddress = mockReturnValue(addressModel);

const wallets = await user.loadWallets();
const wallet = wallets[walletId];
Expand Down Expand Up @@ -288,4 +281,90 @@ describe("User Class", () => {
await expect(user.loadWallets()).rejects.toThrow(new ArgumentError("Malformed backup data"));
});
});

describe(".getWallets", () => {
let user: User;
let walletId: string;
let walletModelWithDefaultAddress: WalletModel;
let addressListModel: AddressList;

beforeEach(() => {
jest.clearAllMocks();
walletId = crypto.randomUUID();
const addressModel1: AddressModel = newAddressModel(walletId);
const addressModel2: AddressModel = newAddressModel(walletId);
walletModelWithDefaultAddress = {
id: walletId,
network_id: Coinbase.networkList.BaseSepolia,
default_address: addressModel1,
};
addressListModel = {
data: [addressModel1, addressModel2],
has_more: false,
next_page: "",
total_count: 1,
};
Coinbase.apiClients.wallet = walletsApiMock;
Coinbase.apiClients.address = addressesApiMock;
const mockUserModel: UserModel = {
id: "12345",
} as UserModel;
user = new User(mockUserModel);
});

it("should raise an error when the Wallet API call fails", async () => {
Coinbase.apiClients.wallet!.listWallets = mockReturnRejectedValue(new Error("API Error"));
await expect(user.getWallets()).rejects.toThrow(new Error("API Error"));
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledTimes(0);
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledWith(10, "");
});

it("should raise an error when the Address API call fails", async () => {
Coinbase.apiClients.wallet!.listWallets = mockReturnValue({
data: [walletModelWithDefaultAddress],
has_more: false,
next_page: "",
total_count: 1,
});
Coinbase.apiClients.address!.listAddresses = mockReturnRejectedValue(new Error("API Error"));
await expect(user.getWallets()).rejects.toThrow(new Error("API Error"));
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledTimes(1);
});

it("should return an empty list of Wallets when the User has no Wallets", async () => {
Coinbase.apiClients.wallet!.listWallets = mockReturnValue({
data: [],
has_more: false,
next_page: "",
total_count: 0,
});
const wallets = await user.getWallets();
expect(wallets.length).toBe(0);
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledTimes(0);
});

it("should return the list of Wallets", async () => {
Coinbase.apiClients.wallet!.listWallets = mockReturnValue({
data: [walletModelWithDefaultAddress],
has_more: false,
next_page: "",
total_count: 2,
});
Coinbase.apiClients.address!.listAddresses = mockReturnValue(addressListModel);
const wallets = await user.getWallets();
expect(wallets.length).toBe(1);
expect(wallets[0].getId()).toBe(walletId);
expect(wallets[0].getAddresses().length).toBe(2);
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.address!.listAddresses).toHaveBeenCalledWith(
walletId,
Wallet.MAX_ADDRESSES,
);
expect(Coinbase.apiClients.wallet!.listWallets).toHaveBeenCalledWith(10, "");
});
});
});
8 changes: 7 additions & 1 deletion src/coinbase/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ import {
} from "../../client";
import { BASE_PATH } from "../../client/base";
import { Coinbase } from "../coinbase";
import { registerAxiosInterceptors } from "../utils";
import { convertStringToHex, registerAxiosInterceptors } from "../utils";
import { HDKey } from "@scure/bip32";

export const mockFn = (...args) => jest.fn(...args) as any;
export const mockReturnValue = data => jest.fn().mockResolvedValue({ data });
export const mockReturnRejectedValue = data => jest.fn().mockRejectedValue(data);

export const getAddressFromHDKey = (hdKey: HDKey): string => {
return new ethers.Wallet(convertStringToHex(hdKey.privateKey!)).address;
};

export const walletId = randomUUID();

export const generateRandomHash = (length = 8) => {
Expand Down Expand Up @@ -121,6 +126,7 @@ export const usersApiMock = {
export const walletsApiMock = {
getWallet: jest.fn(),
createWallet: jest.fn(),
listWallets: jest.fn(),
listWalletBalances: jest.fn(),
getWalletBalance: jest.fn(),
};
Expand Down
81 changes: 46 additions & 35 deletions src/coinbase/tests/wallet_test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { HDKey } from "@scure/bip32";
import * as bip39 from "bip39";
import { randomUUID } from "crypto";
import Decimal from "decimal.js";
import { Address } from "../address";
import { Coinbase } from "../coinbase";
import { ArgumentError } from "../errors";
import { Wallet } from "../wallet";
import {
AddressBalanceList,
Address as AddressModel,
Wallet as WalletModel,
Balance as BalanceModel,
Wallet as WalletModel,
} from "./../../client";
import { ArgumentError } from "../errors";
import {
addressesApiMock,
getAddressFromHDKey,
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 @@ -69,20 +72,37 @@ describe("Wallet Class", () => {
it("should return the correct default address", async () => {
expect(wallet.defaultAddress()?.getId()).toBe(walletModel.default_address.address_id);
});

it("should return true for canSign when the wallet is initialized without a seed", async () => {
expect(wallet.canSign()).toBe(true);
});

it("should create new address and update the existing address list", async () => {
const newAddress = await wallet.createAddress();
expect(newAddress).toBeInstanceOf(Address);
expect(wallet.getAddresses().length).toBe(2);
expect(wallet.getAddress(newAddress.getId()).getId()).toBe(newAddress.getId());
expect(Coinbase.apiClients.address!.createAddress).toHaveBeenCalledTimes(2);
});
});

describe(".init", () => {
const existingSeed =
"hidden assault maple cheap gentle paper earth surprise trophy guide room tired";
const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(existingSeed));
const wallet1 = baseWallet.deriveChild(0);
const address1 = getAddressFromHDKey(wallet1);
const wallet2 = baseWallet.deriveChild(1);
const address2 = getAddressFromHDKey(wallet2);
const addressList = [
{
address_id: "0x23626702fdC45fc75906E535E38Ee1c7EC0C3213",
address_id: address1,
network_id: Coinbase.networkList.BaseSepolia,
public_key: "0x032c11a826d153bb8cf17426d03c3ffb74ea445b17362f98e1536f22bcce720772",
wallet_id: walletId,
},
{
address_id: "0x770603171A98d1CD07018F7309A1413753cA0018",
address_id: address2,
network_id: Coinbase.networkList.BaseSepolia,
public_key: "0x03c3379b488a32a432a4dfe91cc3a28be210eddc98b2005bb59a4cf4ed0646eb56",
wallet_id: walletId,
Expand All @@ -91,29 +111,7 @@ describe("Wallet Class", () => {

beforeEach(async () => {
jest.clearAllMocks();
const getAddress = jest.fn();
addressList.forEach(() => {
getAddress.mockImplementationOnce((wallet_id, address_id) => {
return Promise.resolve({
data: {
address_id,
network_id: Coinbase.networkList.BaseSepolia,
public_key: "0x03c3379b488a32a432a4dfe91cc3a28be210eddc98b2005bb59a4cf4ed0646eb56",
wallet_id,
},
});
});
});
Coinbase.apiClients.address!.getAddress = getAddress;
wallet = await Wallet.init(walletModel, existingSeed, 2);
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(2);
addressList.forEach((address, callIndex) => {
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenNthCalledWith(
callIndex + 1,
walletId,
address.address_id,
);
});
wallet = await Wallet.init(walletModel, existingSeed, addressList);
});

it("should return a Wallet instance", async () => {
Expand All @@ -133,7 +131,19 @@ describe("Wallet Class", () => {
});

it("should derive the correct number of addresses", async () => {
expect(wallet.addresses.length).toBe(2);
expect(wallet.getAddresses().length).toBe(2);
});

it("should derive the correct addresses", async () => {
expect(wallet.getAddress(address1)).toBe(wallet.addresses[0]);
expect(wallet.getAddress(address2)).toBe(wallet.addresses[1]);
});

it("should create new address and update the existing address list", async () => {
const newAddress = await wallet.createAddress();
expect(newAddress).toBeInstanceOf(Address);
expect(wallet.getAddresses().length).toBe(3);
expect(wallet.getAddress(newAddress.getId()).getId()).toBe(newAddress.getId());
});

it("should return the correct string representation", async () => {
Expand All @@ -153,7 +163,6 @@ describe("Wallet Class", () => {
let walletModel: WalletModel;
let seedWallet: Wallet;
const seed = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const addressCount = 1;

beforeAll(async () => {
walletId = randomUUID();
Expand All @@ -167,22 +176,24 @@ describe("Wallet Class", () => {
Coinbase.apiClients.address!.getAddress = mockFn(() => {
return { data: addressModel };
});
seedWallet = await Wallet.init(walletModel, seed, addressCount);
seedWallet = await Wallet.init(walletModel, seed);
});

it("exports the Wallet data", () => {
const walletData = seedWallet.export();
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(1);
expect(walletData.walletId).toBe(seedWallet.getId());
expect(walletData.seed).toBe(seed);
});

it("allows for re-creation of a Wallet", async () => {
const walletData = seedWallet.export();
const newWallet = await Wallet.init(walletModel, walletData.seed, addressCount);
expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(2);
const newWallet = await Wallet.init(walletModel, walletData.seed);
expect(newWallet).toBeInstanceOf(Wallet);
});

it("should return true for canSign when the wallet is initialized with a seed", () => {
expect(wallet.canSign()).toBe(true);
});
});

describe("#defaultAddress", () => {
Expand Down
Loading
Loading