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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Coinbase Node.js SDK Changelog

## [0.0.15]
## [0.0.15]

### Added
### Added

- USD value conversion details to the StakingReward object
- Add Function listHistoricalBalances for Adress for fetching historical balances for an asset

## [0.0.14] - 2024-08-05

Expand Down
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.

69 changes: 68 additions & 1 deletion src/coinbase/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Asset } from "./asset";
import { Balance } from "./balance";
import { BalanceMap } from "./balance_map";
import { FaucetTransaction } from "./faucet_transaction";
import { Amount, StakeOptionsMode } from "./types";
import { HistoricalBalance } from "./historical_balance";
import { Amount, StakeOptionsMode, ListHistoricalBalancesResult } from "./types";
import { formatDate, getWeekBackDate } from "./utils";
import { StakingRewardFormat } from "../client";
import { StakingReward } from "./staking_reward";
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,70 @@ 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<ListHistoricalBalancesResult> {
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 {
historicalBalances: historyList,
nextPageToken: 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 {
historicalBalances: historyList,
nextPageToken: "",
};
}

/**
* 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,
);
}
}
31 changes: 31 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 @@ -34,6 +35,7 @@ import {
} from "./../client/api";
import { Address } from "./address";
import { Wallet } from "./wallet";
import { HistoricalBalance } from "./historical_balance";

export type AssetAPIClient = {
/**
Expand Down Expand Up @@ -334,6 +336,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 Expand Up @@ -758,3 +781,11 @@ export type CreateTradeOptions = {
timeoutSeconds?: number;
intervalSeconds?: number;
};

/**
* Result of ListHistoricalBalances.
*/
export type ListHistoricalBalancesResult = {
historicalBalances: HistoricalBalance[];
nextPageToken: string;
};
125 changes: 124 additions & 1 deletion src/tests/address_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { Coinbase } from "../coinbase/coinbase";
import { Address } from "../index";
import { VALID_ADDRESS_MODEL } from "./utils";
import {
AddressHistoricalBalanceList,
} from "../client";
import {
VALID_ADDRESS_MODEL,
mockReturnValue,
newAddressModel,
externalAddressApiMock,
} from "./utils";
import Decimal from "decimal.js";
import { randomUUID } from "crypto";

describe("Address", () => {
const newAddress = newAddressModel("", randomUUID(), Coinbase.networks.EthereumHolesky);

const address = new Address(newAddress.network_id, newAddress.address_id);

describe(".getNetworkId", () => {
it("should get the network ID", () => {
const address = new Address(VALID_ADDRESS_MODEL.network_id, VALID_ADDRESS_MODEL.address_id);
Expand All @@ -22,4 +37,112 @@ describe("Address", () => {
);
});
});

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 historicalBalancesResult = await address.listHistoricalBalances(Coinbase.assets.Usdc);
expect(historicalBalancesResult.historicalBalances.length).toEqual(2);
expect(historicalBalancesResult.historicalBalances[0].amount).toEqual(new Decimal(1));
expect(historicalBalancesResult.historicalBalances[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(historicalBalancesResult.nextPageToken).toEqual("")
});

it("should return empty if no historical balance found", async () => {
Coinbase.apiClients.externalAddress!.listAddressHistoricalBalance = mockReturnValue({
data: [],
has_more: false,
next_page: "",
});
const historicalBalancesResult = await address.listHistoricalBalances(Coinbase.assets.Usdc);
expect(historicalBalancesResult.historicalBalances.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(historicalBalancesResult.nextPageToken).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 historicalBalancesResult = await address.listHistoricalBalances(Coinbase.assets.Usdc, 1);
expect(historicalBalancesResult.historicalBalances.length).toEqual(1);
expect(historicalBalancesResult.historicalBalances[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(historicalBalancesResult.nextPageToken).toEqual("next page")
});
});

});
Loading
Loading