Skip to content

Commit

Permalink
Implementing getWallet functionality (#27)
Browse files Browse the repository at this point in the history
* Adding getWallet functionality
  • Loading branch information
erdimaden authored May 23, 2024
1 parent e131bca commit 3f64c65
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 77 deletions.
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

0 comments on commit 3f64c65

Please sign in to comment.