Skip to content

Commit

Permalink
Implementing Unhydrated wallets (#28)
Browse files Browse the repository at this point in the history
* Implementing Unhydrated wallets 
* Fixed deriveAddress test case and addressIndex issues
* Added address.getTransfers method to return a list of transfers
* Created generateWalletFromSeed function to generate addresses and wallets from a given seed
  • Loading branch information
erdimaden authored May 24, 2024
1 parent 884348d commit 7955e87
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 87 deletions.
53 changes: 42 additions & 11 deletions src/coinbase/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ATOMIC_UNITS_PER_USDC, WEI_PER_ETHER, WEI_PER_GWEI } from "./constants"
*/
export class Address {
private model: AddressModel;
private key: ethers.Wallet;
private key?: ethers.Wallet;

/**
* Initializes a new Address instance.
Expand All @@ -25,14 +25,10 @@ export class Address {
* @param key - The ethers.js Wallet the Address uses to sign data.
* @throws {InternalError} If the model or key is empty.
*/
constructor(model: AddressModel, key: ethers.Wallet) {
constructor(model: AddressModel, key?: ethers.Wallet) {
if (!model) {
throw new InternalError("Address model cannot be empty");
}
if (!key) {
throw new InternalError("Key cannot be empty");
}

this.model = model;
this.key = key;
}
Expand Down Expand Up @@ -76,7 +72,7 @@ export class Address {
*
* @returns {BalanceMap} - The map from asset ID to balance.
*/
async listBalances(): Promise<BalanceMap> {
async getBalances(): Promise<BalanceMap> {
const response = await Coinbase.apiClients.address!.listAddressBalances(
this.model.wallet_id,
this.model.address_id,
Expand All @@ -85,6 +81,38 @@ export class Address {
return BalanceMap.fromBalances(response.data.data);
}

/**
* Returns all of the transfers associated with the address.
*
* @returns {Transfer[]} The list of transfers.
*/
async getTransfers(): Promise<Transfer[]> {
const transfers: Transfer[] = [];
const queue: string[] = [""];

while (queue.length > 0) {
const page = queue.shift();
const response = await Coinbase.apiClients.transfer!.listTransfers(
this.model.wallet_id,
this.model.address_id,
100,
page?.length ? page : undefined,
);

response.data.data.forEach(transferModel => {
transfers.push(Transfer.fromModel(transferModel));
});

if (response.data.has_more) {
if (response.data.next_page) {
queue.push(response.data.next_page);
}
}
}

return transfers;
}

/**
* Returns the balance of the provided asset.
*
Expand Down Expand Up @@ -115,11 +143,11 @@ export class Address {
}

/**
* Sends an amount of an asset to a destination.
* Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
*
* @param amount - The amount to send.
* @param assetId - The asset ID to send.
* @param destination - The destination address.
* @param amount - The amount of the Asset to send.
* @param assetId - The ID of the Asset to send. For Ether, Coinbase.assetList.Eth, Coinbase.assetList.Gwei, and Coinbase.assetList.Wei supported.
* @param destination - The destination of the transfer. If a Wallet, sends to the Wallet's default address. If a String, interprets it as the address ID.
* @param intervalSeconds - The interval at which to poll the Network for Transfer status, in seconds.
* @param timeoutSeconds - The maximum amount of time to wait for the Transfer to complete, in seconds.
* @returns The transfer object.
Expand All @@ -134,6 +162,9 @@ export class Address {
intervalSeconds = 0.2,
timeoutSeconds = 10,
): Promise<Transfer> {
if (!this.key) {
throw new InternalError("Cannot transfer from address without private key loaded");
}
let normalizedAmount = new Decimal(amount.toString());

const currentBalance = await this.getBalance(assetId);
Expand Down
65 changes: 63 additions & 2 deletions src/coinbase/tests/address_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Address } from "./../address";
import * as crypto from "crypto";
import { ethers } from "ethers";
import { FaucetTransaction } from "./../faucet_transaction";
import { Balance as BalanceModel } from "../../client";
import { Balance as BalanceModel, TransferList } from "../../client";
import Decimal from "decimal.js";
import { APIError, FaucetLimitReachedError } from "../api_error";
import { Coinbase } from "../coinbase";
Expand All @@ -19,6 +19,7 @@ import {
transfersApiMock,
} from "./utils";
import { ArgumentError } from "../errors";
import { Transfer } from "../transfer";

// Test suite for Address class
describe("Address", () => {
Expand Down Expand Up @@ -68,7 +69,7 @@ describe("Address", () => {
});

it("should return the correct list of balances", async () => {
const balances = await address.listBalances();
const balances = await address.getBalances();
expect(balances.get(Coinbase.assetList.Eth)).toEqual(new Decimal(1));
expect(balances.get("usdc")).toEqual(new Decimal(5000));
expect(balances.get("weth")).toEqual(new Decimal(3));
Expand Down Expand Up @@ -252,6 +253,19 @@ describe("Address", () => {
).rejects.toThrow(APIError);
});

it("should throw an InternalError if the address key is not provided", async () => {
const addressWithoutKey = new Address(VALID_ADDRESS_MODEL, null!);
await expect(
addressWithoutKey.createTransfer(
weiAmount,
Coinbase.assetList.Wei,
destination,
intervalSeconds,
timeoutSeconds,
),
).rejects.toThrow(InternalError);
});

it("should throw an APIError if the broadcastTransfer API call fails", async () => {
Coinbase.apiClients.transfer!.createTransfer = mockReturnValue(VALID_TRANSFER_MODEL);
Coinbase.apiClients.transfer!.broadcastTransfer = mockReturnRejectedValue(
Expand Down Expand Up @@ -305,4 +319,51 @@ describe("Address", () => {
jest.restoreAllMocks();
});
});

describe(".getTransfers", () => {
beforeEach(() => {
jest.clearAllMocks();
const pages = ["abc", "def"];
const response = {
data: [VALID_TRANSFER_MODEL],
has_more: false,
next_page: "",
total_count: 0,
} as TransferList;
Coinbase.apiClients.transfer!.listTransfers = mockFn((walletId, addressId) => {
VALID_TRANSFER_MODEL.wallet_id = walletId;
VALID_TRANSFER_MODEL.address_id = addressId;
response.next_page = pages.shift() as string;
response.data = [VALID_TRANSFER_MODEL];
response.has_more = !!response.next_page;
return { data: response };
});
});

it("should return the list of transfers", async () => {
const transfers = await address.getTransfers();
expect(transfers).toHaveLength(3);
expect(transfers[0]).toBeInstanceOf(Transfer);
expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledTimes(3);
expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledWith(
address.getWalletId(),
address.getId(),
100,
undefined,
);
expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledWith(
address.getWalletId(),
address.getId(),
100,
"abc",
);
});

it("should raise an APIError when the API call fails", async () => {
jest.clearAllMocks();
Coinbase.apiClients.transfer!.listTransfers = mockReturnRejectedValue(new APIError(""));
await expect(address.getTransfers()).rejects.toThrow(APIError);
expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledTimes(1);
});
});
});
Loading

0 comments on commit 7955e87

Please sign in to comment.