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

List staking balances #134

Merged
merged 29 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Add Function `listHistoricalBalances` for `Address` for fetching historical balances for an asset
- Support for retrieving historical staking balances information

## [0.0.15]

Expand Down
161 changes: 161 additions & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,31 @@ export interface FetchStakingRewardsRequest {
'format': StakingRewardFormat;
}

/**
*
* @export
* @interface FetchHistoricalStakingBalances200Response
*/
export interface FetchHistoricalStakingBalances200Response {
/**
*
* @type {Array<StakingBalance>}
* @memberof FetchHistoricalStakingBalances200Response
*/
'data': Array<StakingBalance>;
/**
* True if this list has another page of items after this one that can be fetched.
* @type {boolean}
* @memberof FetchHistoricalStakingBalances200Response
*/
'has_more': boolean;
/**
* The page token to be used to fetch the next page.
* @type {string}
* @memberof FetchHistoricalStakingBalances200Response
*/
'next_page': string;
}

/**
*
Expand Down Expand Up @@ -1381,6 +1406,44 @@ export interface StakingRewardUSDValue {
*/
'conversion_time': string;
}
/**
* The staking balances for an address.
* @export
* @interface StakingBalance
*/
export interface StakingBalance {
/**
* The onchain address for which the staking balances are being fetched.
* @type {string}
* @memberof StakingBalance
*/
'address': string;
/**
* The date of the staking balance in format \'YYYY-MM-DD\' in UTC.
* @type {string}
* @memberof StakingBalance
*/
'date': string;
/**
* The bonded stake.
* @type {Balance}
* @memberof StakingBalance
*/
'bonded_stake': Balance;
/**
* The unbonded balance.
* @type {Balance}
* @memberof StakingBalance
*/
'unbonded_balance': Balance;
/**
* The participant type of the given address.
* @type {string}
* @memberof StakingBalance
*/
'participant_type': string;
}

/**
* A trade of an asset to another asset
* @export
Expand Down Expand Up @@ -4110,6 +4173,69 @@ export const StakeApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
* Fetch historical staking balances for a given address
* @summary Fetch historical staking balances
* @param {string} address The address to fetch the historical staking balances for
* @param {string} networkId The ID of the blockchain network
* @param {string} assetId The ID of the asset
* @param {string} startTime The start time of the staking balances period
* @param {string} endTime The end time of the stake balances period
* @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 50.
* @param {string} [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}
*/
fetchHistoricalStakingBalances: async (address: string, networkId: string, assetId: string, startTime: string, endTime: string, limit?: number, page?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'networkId' is not null or undefined
assertParamExists('fetchHistoricalStakingBalances', 'networkId', networkId)
// verify required parameter 'address' is not null or undefined
assertParamExists('fetchHistoricalStakingBalances', 'address', address)
// verify required parameter 'assetId' is not null or undefined
assertParamExists('fetchHistoricalStakingBalances', 'assetId', assetId)
// verify required parameter 'startTime' is not null or undefined
assertParamExists('fetchHistoricalStakingBalances', 'startTime', startTime)
// verify required parameter 'endTime' is not null or undefined
assertParamExists('fetchHistoricalStakingBalances', 'endTime', endTime)

const localVarPath = `/v1/networks/{network_id}/addresses/{address_id}/stake/balances`
.replace(`{${"network_id"}}`, encodeURIComponent(String(networkId)))
.replace(`{${"address_id"}}`, encodeURIComponent(String(address)))
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;

localVarQueryParameter['asset_id'] = assetId;
localVarQueryParameter['start_time'] = startTime;
localVarQueryParameter['end_time'] = endTime

if (limit !== undefined) {
localVarQueryParameter['limit'] = limit;
}

if (page !== undefined) {
localVarQueryParameter['page'] = page;
}

localVarHeaderParameter['Content-Type'] = 'application/json';

setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(null, localVarRequestOptions, configuration)

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Get the latest state of a staking operation
* @summary Get the latest state of a staking operation
Expand Down Expand Up @@ -4299,6 +4425,25 @@ export const StakeApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['StakeApi.fetchStakingRewards']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Fetch historical staking balances for given address
* @summary Fetch historical staking balances
* @param {string} address The address to fetch historical staking balances for
* @param {string} networkId The ID of the blockchain network
* @param {string} assetId The ID of the asset
* @param {string} startTime The start time of the staking balances
* @param {string} endTime The end time of the staking balances
* @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 50.
* @param {string} [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}
*/
async fetchHistoricalStakingBalances(address: string, networkId: string, assetId: string, startTime: string, endTime: string, limit?: number, page?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FetchHistoricalStakingBalances200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.fetchHistoricalStakingBalances(address, networkId, assetId, startTime, endTime, limit, page, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['StakeApi.fetchHistoricalStakingBalances']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Get the latest state of a staking operation
* @summary Get the latest state of a staking operation
Expand Down Expand Up @@ -4399,6 +4544,22 @@ export const StakeApiFactory = function (configuration?: Configuration, basePath
fetchStakingRewards(fetchStakingRewardsRequest: FetchStakingRewardsRequest, limit?: number, page?: string, options?: any): AxiosPromise<FetchStakingRewards200Response> {
return localVarFp.fetchStakingRewards(fetchStakingRewardsRequest, limit, page, options).then((request) => request(axios, basePath));
},
/**
* Fetch historical staking balances for given address
* @summary Fetch historical staking balances
* @param {string} address The onchain address for which the staking balances are being fetched
* @param {string} networkId The ID of the blockchain network
* @param {string} assetId The ID of the asset
* @param {string} startTime The start time of the staking balances period
* @param {string} endTime The end time of the staking balances period
* @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 50.
* @param {string} [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}
*/
fetchHistoricalStakingBalances(address: string, networkId: string, assetId: string, startTime: string, endTime: string, limit?: number, page?: string, options?: any): AxiosPromise<FetchHistoricalStakingBalances200Response> {
return localVarFp.fetchHistoricalStakingBalances(address, networkId, assetId, startTime, endTime, limit, page, options).then((request) => request(axios, basePath));
},
/**
* Get the latest state of a staking operation
* @summary Get the latest state of a staking operation
Expand Down
23 changes: 23 additions & 0 deletions src/coinbase/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { formatDate, getWeekBackDate } from "./utils";
import { StakingRewardFormat } from "../client";
import { StakingReward } from "./staking_reward";
import { StakingBalance } from "./staking_balance";

/**
* A representation of a blockchain address, which is a user-controlled account on a network.
Expand Down Expand Up @@ -177,6 +178,28 @@ export class Address {
);
}

/**
* Lists the historical staking balances for the address.
*
* @param assetId - The asset ID.
* @param startTime - The start time.
* @param endTime - The end time.
* @returns The staking balances.
*/
public async historicalStakingBalances(
assetId: string,
startTime = getWeekBackDate(new Date()),
endTime = formatDate(new Date()),
): Promise<StakingBalance[]> {
return StakingBalance.list(
Coinbase.normalizeNetwork(this.getNetworkId()),
assetId,
this.getId(),
startTime,
endTime,
);
}

/**
* Get the stakeable balance for the supplied asset.
*
Expand Down
20 changes: 20 additions & 0 deletions src/coinbase/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,24 @@ export class Balance {
asset,
);
}

/**
* Converts a BalanceModel of which the amount is in the most common denomination such as ETH, BTC, etc.
*
* @param {BalanceModel} model - The balance model object.
* @returns {Balance} The Balance object.
*/
public static fromModelWithAmountInWholeUnits(model: BalanceModel): Balance {
const asset = Asset.fromModel(model.asset);
return new Balance(new Decimal(model.amount), asset.getAssetId(), asset);
}

/**
* Print the Balance as a string.
*
* @returns The string representation of the Balance.
*/
public toString(): string {
return `Balance { amount: '${this.amount}' asset: '${this.asset?.toString()}' }`;
}
}
120 changes: 120 additions & 0 deletions src/coinbase/staking_balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { StakingBalance as StakingBalanceModel } from "../client";
import { Balance } from "./balance";
import { Coinbase } from "./coinbase";

/**
* A representation of the staking balance for a given asset on a specific date.
*/
export class StakingBalance {
private model: StakingBalanceModel;

/**
* Creates the StakingBalance object.
*
* @param model - The underlying staking balance object.
*/
constructor(model: StakingBalanceModel) {
this.model = model;
}

/**
* Returns a list of StakingBalances for the provided network, asset, and address.
*
* @param networkId - The network ID.
* @param assetId - The asset ID.
* @param address - The address ID.
* @param startTime - The start time.
* @param endTime - The end time.
* @returns The staking balances.
*/
public static async list(
Comment on lines +20 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a method on the Address class (ref) instead of static method here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we have an equivalent for listing staking rewards , I think we need both

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. That pattern is fine. Can you please add the Address class change to this PR.

Copy link
Contributor Author

@marchsu marchsu Aug 12, 2024

Choose a reason for hiding this comment

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

added, wondering if I can test the change to Address, currently only unit tests added to all related files including Wallet, External Address & Wallet Address
hardcoded the address in the list and verify the call from address functions well

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool. Thanks! You can also call this method on an ExternalAddress subclass instance if you want to test on a specific address.

Copy link
Contributor Author

@marchsu marchsu Aug 12, 2024

Choose a reason for hiding this comment

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

thanks, that works as well

const addr = new ExternalAddress(Coinbase.networks.EthereumMainnet,  "0x80000001677f23a227dfed6f61b132d114be83b8ad0aa5f3c5d1d77e6ee0bf5f73b0af750cc34e8f2dae73c21dc36f4a")

addr.historicalStakingBalances(Coinbase.assets.Eth, "2024-07-01T00:00:00Z", "2024-07-03T00:00:00Z").then(balances => {
    balances.forEach(balance => {console.log(balance)});
} ).catch(error => {
    console.log(error);
});
StakingBalance {
  model: {
    address: '0x80000001677f23a227dfed6f61b132d114be83b8ad0aa5f3c5d1d77e6ee0bf5f73b0af750cc34e8f2dae73c21dc36f4a',
    bonded_stake: { amount: '32', asset: [Object] },
    date: '2024-07-02',
    participant_type: 'VALIDATOR',
    unbonded_balance: { amount: '5.479839548', asset: [Object] }
  }
}

toString()

StakingBalance { date: '2024-07-02T00:00:00.000Z' address: '0x80000001677f23a227dfed6f61b132d114be83b8ad0aa5f3c5d1d77e6ee0bf5f73b0af750cc34e8f2dae73c21dc36f4a' bondedStake: 'Balance { amount: '32' asset: 'Asset{ networkId: ethereum-mainnet, assetId: eth, contractAddress: undefined, decimals: 18 }' }' unbondedBalance: 'Balance { amount: '5.479839548' asset: 'Asset{ networkId: ethereum-mainnet, assetId: eth, contractAddress: undefined, decimals: 18 }' }' participantType: 'VALIDATOR' }

networkId: string,
assetId: string,
address: string,
startTime: string,
endTime: string,
): Promise<StakingBalance[]> {
const stakingBalances: StakingBalance[] = [];
const queue: string[] = [""];

while (queue.length > 0) {
const page = queue.shift();

const response = await Coinbase.apiClients.stake!.fetchHistoricalStakingBalances(
address,
networkId,
assetId,
startTime,
endTime,
100,
page?.length ? page : undefined,
);

response.data.data.forEach(stakingBalance => {
stakingBalances.push(new StakingBalance(stakingBalance));
});

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

return stakingBalances;
}

/**
* Returns the bonded stake amount of the StakingBalance.
*
* @returns The Balance.
*/
public bondedStake(): Balance {
return Balance.fromModelWithAmountInWholeUnits(this.model.bonded_stake);
}

/**
* Returns the unbonded stake amount of the StakingBalance.
*
* @returns The Balance.
*/
public unbondedBalance(): Balance {
return Balance.fromModelWithAmountInWholeUnits(this.model.unbonded_balance);
}

/**
* Returns the participant type of the address.
*
* @returns The participant type.
*/
public participantType(): string {
return this.model.participant_type;
}

/**
* Returns the date of the StakingBalance.
*
* @returns The date.
*/
public date(): Date {
return new Date(this.model.date);
}

/**
* Returns the onchain address of the StakingBalance.
*
* @returns The onchain address.
*/
public address(): string {
return this.model.address;
}

/**
* Print the Staking Balance as a string.
*
* @returns The string representation of the Staking Balance.
*/
public toString(): string {
return `StakingBalance { date: '${this.date().toISOString()}' address: '${this.address()}' bondedStake: '${this.bondedStake().toString()}' unbondedBalance: '${this.unbondedBalance().toString()}' participantType: '${this.participantType()}' }`;
}
}
Loading
Loading