Skip to content

Commit

Permalink
feat: add new apy sort field to stake pools
Browse files Browse the repository at this point in the history
  • Loading branch information
lgobbi-atix committed Jun 21, 2022
1 parent ffa6830 commit a3245c7
Show file tree
Hide file tree
Showing 20 changed files with 339,472 additions and 81,140 deletions.
@@ -1,4 +1,4 @@
import { CommonPoolInfo, PoolData, PoolMetrics, PoolSortType } from './types';
import { CommonPoolInfo, PoolAPY, PoolData, PoolMetrics, PoolSortType } from './types';
import { DbSyncProvider } from '../../DbSyncProvider';
import { Logger, dummyLogger } from 'ts-log';
import { Pool } from 'pg';
Expand Down Expand Up @@ -29,6 +29,8 @@ export class DbSyncStakePoolProvider extends DbSyncProvider implements StakePool
// Add more cases as more sort types are supported
case 'metrics':
return (options?: StakePoolQueryOptions) => this.#builder.queryPoolMetrics(hashesIds, totalAdaAmount, options);
case 'apy':
return (options?: StakePoolQueryOptions) => this.#builder.queryPoolAPY(hashesIds, options);
case 'data':
default:
return (options?: StakePoolQueryOptions) => this.#builder.queryPoolData(updatesIds, options);
Expand Down Expand Up @@ -85,6 +87,7 @@ export class DbSyncStakePoolProvider extends DbSyncProvider implements StakePool
poolRetirements,
poolRewards,
poolMetrics,
poolAPYs,
totalCount,
lastEpoch
] = await Promise.all([
Expand All @@ -98,12 +101,16 @@ export class DbSyncStakePoolProvider extends DbSyncProvider implements StakePool
sortType === 'metrics'
? (orderedResult as PoolMetrics[])
: this.#builder.queryPoolMetrics(orderedResultHashIds, totalAdaAmount),
sortType === 'apy'
? (orderedResult as PoolAPY[])
: this.#builder.queryPoolAPY(hashesIds, { rewardsHistoryLimit: options?.rewardsHistoryLimit }),
this.#builder.queryTotalCount(query, params),
this.#builder.getLastEpoch()
]);

return toCoreStakePool(orderedResultHashIds, {
lastEpoch,
poolAPYs,
poolDatas,
poolMetrics,
poolOwners,
Expand Down
Expand Up @@ -12,6 +12,8 @@ import {
EpochRewardModel,
OrderByOptions,
OwnerAddressModel,
PoolAPY,
PoolAPYModel,
PoolDataModel,
PoolMetricsModel,
PoolRegistrationModel,
Expand All @@ -28,6 +30,7 @@ import { Pool, QueryResult } from 'pg';
import {
mapAddressOwner,
mapEpochReward,
mapPoolAPY,
mapPoolData,
mapPoolMetrics,
mapPoolRegistration,
Expand Down Expand Up @@ -89,6 +92,16 @@ export class StakePoolBuilder {
})
);
}
public async queryPoolAPY(hashesIds: number[], options?: StakePoolQueryOptions): Promise<PoolAPY[]> {
this.#logger.debug('About to query pools APY');
const defaultSort: OrderByOptions[] = [{ field: 'apy', order: 'desc' }];
const queryWithSortAndPagination = withPagination(
withSort(Queries.findPoolAPY(options?.rewardsHistoryLimit), options?.sort, defaultSort),
options?.pagination
);
const result: QueryResult<PoolAPYModel> = await this.#db.query(queryWithSortAndPagination, [hashesIds]);
return result.rows.map(mapPoolAPY);
}
public async queryPoolData(updatesIds: number[], options?: StakePoolQueryOptions) {
this.#logger.debug('About to query pool data');
const defaultSort: OrderByOptions[] = [
Expand Down
Expand Up @@ -3,6 +3,8 @@ import {
EpochReward,
EpochRewardModel,
OwnerAddressModel,
PoolAPY,
PoolAPYModel,
PoolData,
PoolDataModel,
PoolMetrics,
Expand Down Expand Up @@ -46,6 +48,7 @@ interface ToCoreStakePoolInput {
lastEpoch: number;
poolMetrics: PoolMetrics[];
totalCount: number;
poolAPYs: PoolAPY[];
}

export const toCoreStakePool = (
Expand All @@ -59,24 +62,25 @@ export const toCoreStakePool = (
poolRewards,
lastEpoch,
poolMetrics,
totalCount
totalCount,
poolAPYs
}: ToCoreStakePoolInput
): StakePoolSearchResults => ({
pageResults: poolHashIds
.map((hashId) => {
const poolData = poolDatas.find((data) => data.hashId === hashId);
if (!poolData) return;
const apy = poolAPYs.find((pool) => pool.hashId === hashId)?.apy;
const registrations = poolRegistrations.filter((r) => r.hashId === poolData.hashId);
const retirements = poolRetirements.filter((r) => r.hashId === poolData.hashId);
const metrics = poolMetrics.find((metric) => metric.hashId === poolData.hashId)?.metrics;
const toReturn: Cardano.StakePool = {
cost: poolData.cost,
epochRewards: poolRewards.filter((r) => r.hashId === poolData.hashId).map((reward) => reward.epochReward),
hexId: poolData.hexId,
id: poolData.id,
margin: poolData.margin,
metrics:
poolMetrics.find((metrics) => metrics.hashId === poolData.hashId)?.metrics ||
({} as Cardano.StakePoolMetrics),
metrics: metrics ? { ...metrics, apy } : ({} as Cardano.StakePoolMetrics),
owners: poolOwners.filter((o) => o.hashId === poolData.hashId).map((o) => o.address),
pledge: poolData.pledge,
relays: poolRelays.filter((r) => r.updateId === poolData.updateId).map((r) => r.relay),
Expand Down Expand Up @@ -204,3 +208,8 @@ export const mapPoolMetrics = (poolMetricsModel: PoolMetricsModel): PoolMetrics
export const mapPoolStats = (poolStats: StakePoolStatsModel): StakePoolStats => ({
qty: { active: Number(poolStats.active), retired: Number(poolStats.retired), retiring: Number(poolStats.retiring) }
});

export const mapPoolAPY = (poolAPYModel: PoolAPYModel): PoolAPY => ({
apy: poolAPYModel.apy,
hashId: poolAPYModel.hash_id
});
Expand Up @@ -262,6 +262,7 @@ pool_rewards_per_epoch AS (
JOIN reward
ON reward.earned_epoch = epochs.epoch_no
AND reward.pool_id = ANY($1)
WHERE reward.type = 'member'
GROUP BY reward.pool_id, epochs.epoch_no
),
pool_stake_per_epoch AS (
Expand All @@ -278,7 +279,7 @@ pool_stake_per_epoch AS (
epoch_rewards AS (
SELECT
epochs.epoch_no,
epochs.epoch_length::TEXT,
epochs.epoch_length,
stake.hash_id,
COALESCE(rewards.total_amount, 0) AS total_rewards,
COALESCE(stake.active_stake, 0) AS active_stake,
Expand Down Expand Up @@ -327,11 +328,50 @@ epoch_rewards AS (

export const findPoolEpochRewards = (limit?: number) => `
${epochRewardsSubqueries(limit)}
SELECT *
SELECT
epoch_no,
epoch_length::TEXT,
hash_id,
total_rewards,
active_stake,
operator_fees,
member_roi
FROM epoch_rewards
ORDER BY epoch_no desc
`;

export const findPoolAPY = (limit?: number) => `
${epochRewardsSubqueries(limit)},
avg_daily_roi AS (
SELECT
hash_id,
SUM(member_roi / (epoch_length / 86400000)) / count(1) AS avg_roi
FROM epoch_rewards
GROUP BY hash_id
),
pool_apy AS (
SELECT
epochs.hash_id,
POWER(
1 + (avg_daily_roi.avg_roi * (epochs.epoch_length / 86400000)),
365 / (epochs.epoch_length / 86400000)
) - 1 AS apy
FROM epoch_rewards AS epochs
JOIN (
SELECT
hash_id,
MAX(epoch_no) AS epoch_no
FROM epoch_rewards AS sub
GROUP BY hash_id
) AS max_epoch
ON max_epoch.epoch_no = epochs.epoch_no
AND max_epoch.hash_id = epochs.hash_id
JOIN avg_daily_roi
ON avg_daily_roi.hash_id = epochs.hash_id
)
SELECT * FROM pool_apy
`;

export const findPools = `
SELECT
ph.id,
Expand Down Expand Up @@ -790,6 +830,8 @@ export const withSort = (query: string, sort?: StakePoolQueryOptions['sort'], de
return orderBy(query, [mappedSort, { field: 'pool_id', order: 'asc' }]);
case 'metrics':
return orderBy(query, [mappedSort, { field: 'id', order: 'asc' }]);
case 'apy':
return orderBy(query, [mappedSort, { field: 'hash_id', order: 'asc' }]);
default:
return orderBy(query, [mappedSort]);
}
Expand All @@ -800,6 +842,7 @@ const Queries = {
POOLS_WITH_PLEDGE_MET,
STATUS_QUERY,
findLastEpoch,
findPoolAPY,
findPoolEpochRewards,
findPoolStats,
findPools,
Expand Down
Expand Up @@ -131,7 +131,7 @@ export interface PoolMetricsModel {
}

export interface PoolMetrics extends CommonPoolInfo {
metrics: Cardano.StakePoolMetrics;
metrics: Omit<Cardano.StakePoolMetrics, 'apy'>;
}

export interface TotalCountModel {
Expand All @@ -144,7 +144,16 @@ export interface StakePoolStatsModel {
retiring: string;
}

export type PoolSortType = 'data' | 'metrics';
export interface PoolAPYModel {
hash_id: number;
apy: number;
}

export interface PoolAPY extends CommonPoolInfo {
apy: number;
}

export type PoolSortType = 'data' | 'metrics' | 'apy';
export interface OrderByOptions {
field: string;
order: 'asc' | 'desc';
Expand Down
@@ -1,8 +1,15 @@
import { PoolSortType } from './types';
import { ProviderError, ProviderFailure, isPoolDataSortField, isPoolMetricsSortField } from '@cardano-sdk/core';
import {
ProviderError,
ProviderFailure,
isPoolAPYSortField,
isPoolDataSortField,
isPoolMetricsSortField
} from '@cardano-sdk/core';

export const getStakePoolSortType = (field: string): PoolSortType => {
if (isPoolDataSortField(field)) return 'data';
if (isPoolMetricsSortField(field)) return 'metrics';
if (isPoolAPYSortField(field)) return 'apy';
throw new ProviderError(ProviderFailure.Unknown, null, 'Invalid sort field');
};
7 changes: 7 additions & 0 deletions packages/cardano-services/src/StakePool/openApi.json
Expand Up @@ -76,6 +76,13 @@
},
"components": {
"schemas": {
"Undefined": {
"required": ["__type"],
"type": "object",
"properties": {
"__type": { "type": "string", "enum": ["undefined"] }
}
},
"StakePoolStats": {
"type": "object",
"properties": {
Expand Down
Expand Up @@ -45,7 +45,7 @@ Object {
exports[`NetworkInfoHttpService healthy state with NetworkInfoHttpProvider response is an object of network info 1`] = `
Object {
"lovelaceSupply": Object {
"circulating": 69163989759711432n,
"circulating": 69173735562813828n,
"max": 45000000000000000n,
"total": 40342337171803411n,
},
Expand Down
@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Cardano, StakePoolQueryOptions } from '@cardano-sdk/core';
import { Pool } from 'pg';
Expand Down Expand Up @@ -45,7 +46,8 @@ describe('StakePoolBuilder', () => {
});
describe('queryPoolRewards', () => {
it('queryPoolRewards', async () => {
const epochRewards = (await builder.queryPoolRewards([1, 2, 3])).map((eR) => eR?.epochReward);
const epochRewards = await builder.queryPoolRewards([1, 6, 15]);
expect(epochRewards).toHaveLength(3);
expect(epochRewards).toMatchSnapshot();
});
});
Expand Down Expand Up @@ -279,4 +281,38 @@ describe('StakePoolBuilder', () => {
expect(result).toMatchSnapshot();
});
});
describe('queryPoolAPY', () => {
describe('sort', () => {
it('by default sort (APY desc)', async () => {
const result = (await builder.queryPoolAPY([1, 15])).map(({ apy }) => ({ apy }));
expect(result).toHaveLength(2);
expect(result[0].apy).toBeGreaterThan(result[1].apy);
expect(result).toMatchSnapshot();
});
it('by APY asc', async () => {
const result = (await builder.queryPoolAPY([1, 15], { sort: { field: 'apy', order: 'asc' } })).map(
({ apy }) => ({ apy })
);
expect(result).toHaveLength(2);
expect(result[0].apy).toBeLessThan(result[1].apy);
expect(result).toMatchSnapshot();
});
});
describe('pagination', () => {
it('with limit', async () => {
const result = (await builder.queryPoolAPY([1, 15, 20], { pagination: { limit: 1, startAt: 0 } })).map(
({ apy }) => ({ apy })
);
expect(result).toHaveLength(1);
expect(result).toMatchSnapshot();
});
it('with startAt', async () => {
const result = (await builder.queryPoolAPY([1, 15, 20], { pagination: { limit: 5, startAt: 1 } })).map(
({ apy }) => ({ apy })
);
expect(result).toHaveLength(2);
expect(result).toMatchSnapshot();
});
});
});
});

0 comments on commit a3245c7

Please sign in to comment.