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

LW-9573 Stake pools sorting #1084

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ProviderError,
ProviderFailure,
QueryStakePoolsArgs,
SortField,
StakePoolProvider,
StakePoolStats
} from '@cardano-sdk/core';
Expand Down Expand Up @@ -80,6 +81,8 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
#responseConfig: StakePoolProviderProps['responseConfig'];
#useBlockfrost: boolean;

static notSupportedSortFields: SortField[] = ['blocks', 'lastRos', 'liveStake', 'margin', 'pledge', 'ros'];

constructor(
{ paginationPageSizeLimit, responseConfig, useBlockfrost }: StakePoolProviderProps,
{ cache, dbPools, cardanoNode, genesisData, metadataService, logger, epochMonitor }: StakePoolProviderDependencies
Expand Down Expand Up @@ -133,12 +136,6 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
}

return (options: QueryStakePoolsArgs) => this.#builder.queryPoolAPY(hashesIds, this.#epochLength, options);
case 'ros':
throw new ProviderError(
ProviderFailure.NotImplemented,
null,
'DbSyncStakePoolProvider do not support sort by ROS'
);
case 'data':
default:
return (options: QueryStakePoolsArgs) => this.#builder.queryPoolData(updatesIds, useBlockfrost, options);
Expand Down Expand Up @@ -258,7 +255,7 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
}

public async queryStakePools(options: QueryStakePoolsArgs): Promise<Paginated<Cardano.StakePool>> {
const { filters, pagination, apyEpochsBackLimit = APY_EPOCHS_BACK_LIMIT_DEFAULT } = options;
const { filters, pagination, sort, apyEpochsBackLimit = APY_EPOCHS_BACK_LIMIT_DEFAULT } = options;
const useBlockfrost = this.#useBlockfrost;

if (pagination.limit > this.#paginationPageSizeLimit) {
Expand All @@ -279,6 +276,14 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
);
}

if (DbSyncStakePoolProvider.notSupportedSortFields.includes(sort?.field || 'name')) {
throw new ProviderError(
ProviderFailure.NotImplemented,
undefined,
`DbSyncStakePoolProvider doesn't support sort by ${sort?.field} `
);
}

const { params, query } = useBlockfrost
? this.#builder.buildBlockfrostQuery(filters)
: filters?._condition === 'or'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ export const stakePoolSearchSelection = [

export const sortSelectionMap: { [key in SortField]: string } = {
apy: 'metrics_ros',
blocks: 'metrics_minted_blocks',
cost: 'params.cost',
lastRos: 'metrics_last_ros',
liveStake: 'metrics_live_stake',
// PERF: this may be source of performances issue due to its complexity.
// In case of performances degradation we need to keep in mind this.
margin: "(margin->>'numerator')::numeric / (margin->>'denominator')::numeric",
name: 'metadata.name',
pledge: 'params_pledge',
ros: 'metrics_ros',
saturation: 'metrics_live_saturation'
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,14 @@ describe('StakePoolHttpService', () => {
}, dbPools.main)
);

it.each(DbSyncStakePoolProvider.notSupportedSortFields)(
"Doesn't support sorting by %s",
async (field) =>
await expect(provider.queryStakePools({ pagination, sort: { field, order: 'asc' } })).rejects.toThrow(
`DbSyncStakePoolProvider doesn't support sort by ${field}`
)
);

describe('pagination', () => {
const baseArgs = { pagination: { limit: 2, startAt: 0 } };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,125 @@ describe('TypeormStakePoolProvider', () => {
expect(response.pageResults[0].metrics?.ros).toEqual(expectedRos.min);
});
});

describe('sort by margin', () => {
let margins: Cardano.Fraction[] = [];

beforeAll(() => {
margins = poolsInfo
.map(({ margin }) => margin)
.sort((a, b) => (a.numerator / a.denominator < b.numerator / b.denominator ? -1 : 1));
});

it('desc order', async () => {
const { pageResults } = await provider.queryStakePools(
setSortCondition({ pagination }, 'desc', 'margin')
);

expect(pageResults.map(({ margin }) => margin)).toEqual(
margins
.map((_) => _)
.reverse()
.splice(0, 10)
);
});

it('asc order', async () => {
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'margin'));

expect(pageResults.map(({ margin }) => margin)).toEqual(margins.map((_) => _).splice(0, 10));
});
});

describe('sort by pledge', () => {
let pledges: bigint[] = [];

beforeAll(() => {
pledges = poolsInfo.map(({ pledge }) => BigInt(pledge)).sort((a, b) => (a < b ? -1 : 1));
});

it('desc order', async () => {
const { pageResults } = await provider.queryStakePools(
setSortCondition({ pagination }, 'desc', 'pledge')
);

expect(pageResults.map(({ pledge }) => pledge)).toEqual(
pledges
.map((_) => _)
.reverse()
.splice(0, 10)
);
});

it('asc order', async () => {
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'pledge'));

expect(pageResults.map(({ pledge }) => pledge)).toEqual(pledges.map((_) => _).splice(0, 10));
});
});

describe('sort by blocks', () => {
let blocks: number[] = [];

beforeAll(() => {
blocks = poolsInfoWithMetrics.map((_) => _.blocks).sort((a, b) => (a < b ? -1 : 1));
});

it('desc order', async () => {
const { pageResults } = await provider.queryStakePools(
setSortCondition({ pagination }, 'desc', 'blocks')
);

expect(pageResults.map(({ metrics }) => metrics!.blocksCreated)).toEqual(
blocks
.map((_) => _)
.reverse()
.splice(0, 10)
);
});

it('asc order', async () => {
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'blocks'));

expect(pageResults.map(({ metrics }) => metrics!.blocksCreated)).toEqual(
blocks.filter((_) => _ !== null).splice(0, 10)
);
});
});

describe('sort by liveStake', () => {
let pledges: bigint[] = [];

beforeAll(() => {
pledges = poolsInfoWithMetrics
.filter(({ stake }) => stake !== null)
.map(({ stake }) => BigInt(stake))
.sort((a, b) => (a < b ? -1 : 1));
});

it('desc order', async () => {
const { pageResults } = await provider.queryStakePools(
setSortCondition({ pagination }, 'desc', 'liveStake')
);

expect(pageResults.map(({ metrics }) => metrics!.stake.live)).toEqual(
pledges
.map((_) => _)
.reverse()
.splice(0, 10)
);
});

it('asc order', async () => {
const { pageResults } = await provider.queryStakePools(
setSortCondition({ pagination }, 'asc', 'liveStake')
);

expect(pageResults.map(({ metrics }) => metrics!.stake.live)).toEqual(
pledges.map((_) => _).splice(0, 10)
);
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type PoolInfo = {
apy?: number;
lastRos?: number;
ros?: number;
pledge: string;
blocks: number;
stake: string;
margin: Cardano.Fraction;
};

export type PoolFixtureModel = {
Expand All @@ -29,6 +33,10 @@ export type PoolFixtureModel = {
live_saturation: string;
ros?: string;
last_ros?: string;
pledge: string;
blocks: number;
stake: string;
margin: Cardano.Fraction;
};

export class TypeormStakePoolFixtureBuilder {
Expand All @@ -54,16 +62,36 @@ export class TypeormStakePoolFixtureBuilder {
this.#logger.warn(`${desiredQty} pools desired, only ${resultsQty} results found`);
}

return result.rows.map(({ id, status, name, ticker, cost, ros, last_ros, live_saturation, metadata_url }) => ({
cost,
id: id as unknown as Cardano.PoolId,
lastRos: typeof last_ros === 'string' ? Number.parseFloat(last_ros) : undefined,
metadataUrl: metadata_url,
name,
ros: typeof ros === 'string' ? Number.parseFloat(ros) : undefined,
saturation: Number.parseFloat(live_saturation),
status,
ticker
}));
return result.rows.map(
({
id,
status,
name,
ticker,
cost,
ros,
last_ros,
live_saturation,
metadata_url,
pledge,
blocks,
stake,
margin
}) => ({
blocks,
cost,
id: id as unknown as Cardano.PoolId,
lastRos: typeof last_ros === 'string' ? Number.parseFloat(last_ros) : undefined,
margin,
metadataUrl: metadata_url,
name,
pledge,
ros: typeof ros === 'string' ? Number.parseFloat(ros) : undefined,
saturation: Number.parseFloat(live_saturation),
stake,
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment here, might be better to call this live_stake or liveStake depending on casing required for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above

status,
ticker
})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ export const withMeta = (withMetadata: boolean) => `AND pr.metadata_url IS ${wit

// Query returns all pools except delisted
export const findStakePools = (hasMetadata = false) => `
SELECT
sp.id, sp.status, pr.reward_account, pr.pledge,
pr.cost, pr.margin,pr.relays, pr.owners, pr.vrf,
pr.metadata_url, pr.metadata_hash, pr.block_slot,
pm.name, pm.ticker, pm.description, pm.homepage,
pm.ext, cpm.live_saturation, cpm.last_ros, cpm.ros
SELECT
sp.id, sp.status, pr.reward_account, pr.pledge,
pr.cost, pr.margin, pr.relays, pr.owners, pr.vrf,
pr.metadata_url, pr.metadata_hash, pr.block_slot,
pm.name, pm.ticker, pm.description, pm.homepage,
pm.ext, cpm.live_saturation, cpm.last_ros, cpm.ros,
cpm.minted_blocks AS blocks, cpm.live_stake AS stake
Copy link
Contributor

Choose a reason for hiding this comment

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

it might be better to use cpm.live_stake AS live_stake so that it's clear when pulling results that it's referring to live, not active.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I checked this better: in /src/ and exposed interfaces the names are correct: live_stake or liveStake; the bad naming is used only in the tests queries... I would opt to save the time for fixing this.
Do you agree?

FROM public.stake_pool as sp
LEFT JOIN pool_registration as pr ON sp.last_registration_id = pr.id
LEFT JOIN pool_metadata as pm ON pr.id = pm.pool_update_id
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Provider/StakePoolProvider/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const PoolDataSortFields = ['name', 'cost'] as const;
export const PoolMetricsSortFields = ['saturation'] as const;
export const PoolDataSortFields = ['cost', 'name', 'margin', 'pledge'] as const;
export const PoolMetricsSortFields = ['blocks', 'liveStake', 'saturation'] as const;
export const PoolAPYSortFields = ['apy'] as const;
export const PoolROSSortFields = ['ros', 'lastRos'] as const;

Expand Down