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

[Balance] fetch historical balance #140

Closed
wants to merge 14 commits into from
27 changes: 25 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions src/coinbase/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Asset } from "./asset";
import { Balance } from "./balance";
import { BalanceMap } from "./balance_map";
import { FaucetTransaction } from "./faucet_transaction";
import { HistoricalBalance } from "./historical_balance";
import { Amount, StakeOptionsMode } from "./types";
import { formatDate, getWeekBackDate } from "./utils";
import { StakingRewardFormat } from "../client";
Expand All @@ -13,6 +14,8 @@ import { StakingReward } from "./staking_reward";
* A representation of a blockchain address, which is a user-controlled account on a network.
*/
export class Address {
private static MAX_HISTORICAL_BALANCE = 1000;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pick a magic number for fetching all historical balance (fetch without page parameters).


protected networkId: string;
protected id: string;

Expand Down Expand Up @@ -79,6 +82,64 @@ export class Address {
return Balance.fromModelAndAssetId(response.data, assetId).amount;
}

/**
* Returns the historical balances of the provided asset.
*
* @param assetId - The asset ID.
* @param limit - A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.
* @param page - A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results.
* @returns The list of historical balance of the asset and next page token.
*/
public async listHistoricalBalances(
assetId: string,
limit?: number,
page?: string,
): Promise<[HistoricalBalance[], string]> {
xinyu-li-cb marked this conversation as resolved.
Show resolved Hide resolved
const historyList: HistoricalBalance[] = [];

if (limit !== undefined) {
const response = await Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance(
this.getNetworkId(),
this.getId(),
Asset.primaryDenomination(assetId),
limit,
page ? page : undefined,
);
response.data.data.forEach(historicalBalanceModel => {
const historicalBalance = HistoricalBalance.fromModel(historicalBalanceModel);
historyList.push(historicalBalance);
});

return [historyList, response.data.next_page];
}

const queue: string[] = [""];

while (queue.length > 0 && historyList.length < Address.MAX_HISTORICAL_BALANCE) {
const page = queue.shift();
const response = await Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance(
this.getNetworkId(),
this.getId(),
Asset.primaryDenomination(assetId),
100,
page ? page : undefined,
);

response.data.data.forEach(historicalBalanceModel => {
const historicalBalance = HistoricalBalance.fromModel(historicalBalanceModel);
historyList.push(historicalBalance);
});

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

return [historyList, ""];
}

/**
* Lists the staking rewards for the address.
*
Expand Down
44 changes: 44 additions & 0 deletions src/coinbase/historical_balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Decimal from "decimal.js";
import { HistoricalBalance as HistoricalBalanceModel } from "../client";
import { Asset } from "./asset";

/** A representation of historical balance. */
export class HistoricalBalance {
public readonly amount: Decimal;
public readonly blockHash: string;
public readonly blockHeight: Decimal;
public readonly asset: Asset;

/**
* Private constructor to prevent direct instantiation outside of the factory methods.
*
* @ignore
* @param {Decimal} amount - The amount of the balance.
* @param {Decimal} blockHeight - The block height at which the balance was recorded.
* @param {string} blockHash - The block hash at which the balance was recorded
* @param {string} asset - The asset we want to fetch.
* @hideconstructor
*/
private constructor(amount: Decimal, blockHeight: Decimal, blockHash: string, asset: Asset) {
this.amount = amount;
this.blockHeight = blockHeight;
this.blockHash = blockHash;
this.asset = asset;
}

/**
* Converts a BalanceModel into a Balance object.
*
* @param {HistoricalBalanceModel} model - The historical balance model object.
* @returns {HistoricalBalance} The HistoricalBalance object.
*/
public static fromModel(model: HistoricalBalanceModel): HistoricalBalance {
const asset = Asset.fromModel(model.asset);
return new HistoricalBalance(
asset.fromAtomicAmount(new Decimal(model.amount)),
new Decimal(model.block_height),
model.block_hash,
asset,
);
}
}
22 changes: 22 additions & 0 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Address as AddressModel,
AddressList,
AddressBalanceList,
AddressHistoricalBalanceList,
Balance,
CreateAddressRequest,
CreateWalletRequest,
Expand Down Expand Up @@ -334,6 +335,27 @@ export type ExternalAddressAPIClient = {
options?: RawAxiosRequestConfig,
): AxiosPromise<Balance>;

/**
* List the historical balance of an asset in a specific address.
*
* @summary Get address balance history for asset
* @param networkId - The ID of the blockchain network
* @param addressId - The ID of the address to fetch the historical balance for.
* @param assetId - The symbol of the asset to fetch the historical balance for.
* @param limit - A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.
* @param page - A cursor for pagination across multiple pages of results. Don\&#39;t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results.
* @param options - Override http request option.
* @throws {RequiredError}
*/
listAddressHistoricalBalance(
networkId: string,
addressId: string,
assetId: string,
limit?: number,
page?: string,
options?: RawAxiosRequestConfig,
): AxiosPromise<AddressHistoricalBalanceList>;

/**
* Request faucet funds to be sent to external address.
*
Expand Down
111 changes: 110 additions & 1 deletion src/tests/external_address_test.ts
xinyu-li-cb marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import {
AddressBalanceList,
Balance,
AddressHistoricalBalanceList,
HistoricalBalance,
FetchStakingRewards200Response,
StakingContext as StakingContextModel,
StakingOperation as StakingOperationModel,
Expand Down Expand Up @@ -439,6 +441,113 @@ describe("ExternalAddress", () => {
});
});

describe(".listHistoricalBalance", () => {
beforeEach(() => {
const mockHistoricalBalanceResponse: AddressHistoricalBalanceList = {
data: [
{
amount: "1000000",
block_hash: "0x0dadd465fb063ceb78babbb30abbc6bfc0730d0c57a53e8f6dc778dafcea568f",
block_height:"12345",
asset: {
asset_id: "usdc",
network_id: Coinbase.networks.EthereumHolesky,
decimals: 6,
},
},
{
amount: "5000000",
block_hash: "0x5c05a37dcb4910b22a775fc9480f8422d9d615ad7a6a0aa9d8778ff8cc300986",
block_height:"67890",
asset: {
asset_id: "usdc",
network_id: Coinbase.networks.EthereumHolesky,
decimals: 6,
},
},
],
has_more: false,
next_page: "",
};
Coinbase.apiClients.externalAddress = externalAddressApiMock;
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance =
mockReturnValue(mockHistoricalBalanceResponse);
});

it("should return results with USDC historical balance", async () => {
const [history, nextPage] = await address.listHistoricalBalances(Coinbase.assets.Usdc);
expect(history.length).toEqual(2);
expect(history[0].amount).toEqual(new Decimal(1));
expect(history[1].amount).toEqual(new Decimal(5));
expect(
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance,
).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance).toHaveBeenCalledWith(
address.getNetworkId(),
address.getId(),
Coinbase.assets.Usdc,
100,
undefined,
);
expect(nextPage).toEqual("")
});

it("should return empty if no historical balance found", async () => {
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance = mockReturnValue({
data: [],
has_more: false,
next_page: "",
});
const [history, nextPage] = await address.listHistoricalBalances(Coinbase.assets.Usdc);
expect(history.length).toEqual(0);
expect(
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance,
).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance).toHaveBeenCalledWith(
address.getNetworkId(),
address.getId(),
Coinbase.assets.Usdc,
100,
undefined,
);
expect(nextPage).toEqual("")
});

it("should return results with USDC historical balance and next page", async () => {
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance = mockReturnValue({
data: [
{
amount: "5000000",
block_hash: "0x5c05a37dcb4910b22a775fc9480f8422d9d615ad7a6a0aa9d8778ff8cc300986",
block_height:"67890",
asset: {
asset_id: "usdc",
network_id: Coinbase.networks.EthereumHolesky,
decimals: 6,
},
},
],
has_more: true,
next_page: "next page",
});

const [history, nextPage] = await address.listHistoricalBalances(Coinbase.assets.Usdc, 1);
expect(history.length).toEqual(1);
expect(history[0].amount).toEqual(new Decimal(5));
expect(
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance,
).toHaveBeenCalledTimes(1);
expect(Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance).toHaveBeenCalledWith(
address.getNetworkId(),
address.getId(),
Coinbase.assets.Usdc,
1,
undefined,
);
expect(nextPage).toEqual("next page")
});
});

describe(".getBalance", () => {
beforeEach(() => {
const mockWalletBalance: Balance = {
Expand Down Expand Up @@ -626,4 +735,4 @@ describe("ExternalAddress", () => {
});
});
});
});
});
31 changes: 31 additions & 0 deletions src/tests/historical_balance_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HistoricalBalance } from "../coinbase/historical_balance";
import { HistoricalBalance as HistoricalBalanceModel } from "../client";
import { Decimal } from "decimal.js";
import { Coinbase } from "../coinbase/coinbase";

describe("HistoricalBalance", () => {
describe("#fromModel", () => {
const amount = new Decimal(1);
const historyModel: HistoricalBalanceModel = {
amount: "1000000000000000000",
block_hash: "0x0dadd465fb063ceb78babbb30abbc6bfc0730d0c57a53e8f6dc778dafcea568f",
block_height: "11349306",
asset: {
asset_id: Coinbase.assets.Eth,
network_id: Coinbase.networks.BaseSepolia,
decimals: 18,
contract_address: "0x",
},
};

const historicalBalance = HistoricalBalance.fromModel(historyModel);

it("returns a new Balance object with the correct amount", () => {
expect(historicalBalance.amount).toEqual(amount);
});

it("returns a new Balance object with the correct asset_id", () => {
expect(historicalBalance.asset.assetId).toBe(Coinbase.assets.Eth);
});
});
});
1 change: 1 addition & 0 deletions src/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export const externalAddressApiMock = {
listExternalAddressBalances: jest.fn(),
getExternalAddressBalance: jest.fn(),
requestExternalFaucetFunds: jest.fn(),
listAddressHistoricalBalance: jest.fn(),
};

export const serverSignersApiMock = {
Expand Down
Loading
Loading