Skip to content

Commit f7faf90

Browse files
authored
Merge pull request #1084 from input-output-hk/feat/LW-9573-pool-sort
LW-9573 Stake pools sorting
2 parents bbc894c + bcc5e80 commit f7faf90

File tree

7 files changed

+193
-26
lines changed

7 files changed

+193
-26
lines changed

packages/cardano-services/src/StakePool/DbSyncStakePoolProvider/DbSyncStakePoolProvider.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ProviderError,
1313
ProviderFailure,
1414
QueryStakePoolsArgs,
15+
SortField,
1516
StakePoolProvider,
1617
StakePoolStats
1718
} from '@cardano-sdk/core';
@@ -80,6 +81,8 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
8081
#responseConfig: StakePoolProviderProps['responseConfig'];
8182
#useBlockfrost: boolean;
8283

84+
static notSupportedSortFields: SortField[] = ['blocks', 'lastRos', 'liveStake', 'margin', 'pledge', 'ros'];
85+
8386
constructor(
8487
{ paginationPageSizeLimit, responseConfig, useBlockfrost }: StakePoolProviderProps,
8588
{ cache, dbPools, cardanoNode, genesisData, metadataService, logger, epochMonitor }: StakePoolProviderDependencies
@@ -133,12 +136,6 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
133136
}
134137

135138
return (options: QueryStakePoolsArgs) => this.#builder.queryPoolAPY(hashesIds, this.#epochLength, options);
136-
case 'ros':
137-
throw new ProviderError(
138-
ProviderFailure.NotImplemented,
139-
null,
140-
'DbSyncStakePoolProvider do not support sort by ROS'
141-
);
142139
case 'data':
143140
default:
144141
return (options: QueryStakePoolsArgs) => this.#builder.queryPoolData(updatesIds, useBlockfrost, options);
@@ -258,7 +255,7 @@ export class DbSyncStakePoolProvider extends DbSyncProvider(RunnableModule) impl
258255
}
259256

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

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

279+
if (DbSyncStakePoolProvider.notSupportedSortFields.includes(sort?.field || 'name')) {
280+
throw new ProviderError(
281+
ProviderFailure.NotImplemented,
282+
undefined,
283+
`DbSyncStakePoolProvider doesn't support sort by ${sort?.field} `
284+
);
285+
}
286+
282287
const { params, query } = useBlockfrost
283288
? this.#builder.buildBlockfrostQuery(filters)
284289
: filters?._condition === 'or'

packages/cardano-services/src/StakePool/TypeormStakePoolProvider/util.ts

+6
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@ export const stakePoolSearchSelection = [
5050

5151
export const sortSelectionMap: { [key in SortField]: string } = {
5252
apy: 'metrics_ros',
53+
blocks: 'metrics_minted_blocks',
5354
cost: 'params.cost',
5455
lastRos: 'metrics_last_ros',
56+
liveStake: 'metrics_live_stake',
57+
// PERF: this may be source of performances issue due to its complexity.
58+
// In case of performances degradation we need to keep in mind this.
59+
margin: "(margin->>'numerator')::numeric / (margin->>'denominator')::numeric",
5560
name: 'metadata.name',
61+
pledge: 'params_pledge',
5662
ros: 'metrics_ros',
5763
saturation: 'metrics_live_saturation'
5864
};

packages/cardano-services/test/StakePool/StakePoolHttpService.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,14 @@ describe('StakePoolHttpService', () => {
445445
}, dbPools.main)
446446
);
447447

448+
it.each(DbSyncStakePoolProvider.notSupportedSortFields)(
449+
"Doesn't support sorting by %s",
450+
async (field) =>
451+
await expect(provider.queryStakePools({ pagination, sort: { field, order: 'asc' } })).rejects.toThrow(
452+
`DbSyncStakePoolProvider doesn't support sort by ${field}`
453+
)
454+
);
455+
448456
describe('pagination', () => {
449457
const baseArgs = { pagination: { limit: 2, startAt: 0 } };
450458

packages/cardano-services/test/StakePool/TypeormStakePoolProvider/TypeormStakePoolProvider.test.ts

+119
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,125 @@ describe('TypeormStakePoolProvider', () => {
780780
expect(response.pageResults[0].metrics?.ros).toEqual(expectedRos.min);
781781
});
782782
});
783+
784+
describe('sort by margin', () => {
785+
let margins: Cardano.Fraction[] = [];
786+
787+
beforeAll(() => {
788+
margins = poolsInfo
789+
.map(({ margin }) => margin)
790+
.sort((a, b) => (a.numerator / a.denominator < b.numerator / b.denominator ? -1 : 1));
791+
});
792+
793+
it('desc order', async () => {
794+
const { pageResults } = await provider.queryStakePools(
795+
setSortCondition({ pagination }, 'desc', 'margin')
796+
);
797+
798+
expect(pageResults.map(({ margin }) => margin)).toEqual(
799+
margins
800+
.map((_) => _)
801+
.reverse()
802+
.splice(0, 10)
803+
);
804+
});
805+
806+
it('asc order', async () => {
807+
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'margin'));
808+
809+
expect(pageResults.map(({ margin }) => margin)).toEqual(margins.map((_) => _).splice(0, 10));
810+
});
811+
});
812+
813+
describe('sort by pledge', () => {
814+
let pledges: bigint[] = [];
815+
816+
beforeAll(() => {
817+
pledges = poolsInfo.map(({ pledge }) => BigInt(pledge)).sort((a, b) => (a < b ? -1 : 1));
818+
});
819+
820+
it('desc order', async () => {
821+
const { pageResults } = await provider.queryStakePools(
822+
setSortCondition({ pagination }, 'desc', 'pledge')
823+
);
824+
825+
expect(pageResults.map(({ pledge }) => pledge)).toEqual(
826+
pledges
827+
.map((_) => _)
828+
.reverse()
829+
.splice(0, 10)
830+
);
831+
});
832+
833+
it('asc order', async () => {
834+
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'pledge'));
835+
836+
expect(pageResults.map(({ pledge }) => pledge)).toEqual(pledges.map((_) => _).splice(0, 10));
837+
});
838+
});
839+
840+
describe('sort by blocks', () => {
841+
let blocks: number[] = [];
842+
843+
beforeAll(() => {
844+
blocks = poolsInfoWithMetrics.map((_) => _.blocks).sort((a, b) => (a < b ? -1 : 1));
845+
});
846+
847+
it('desc order', async () => {
848+
const { pageResults } = await provider.queryStakePools(
849+
setSortCondition({ pagination }, 'desc', 'blocks')
850+
);
851+
852+
expect(pageResults.map(({ metrics }) => metrics!.blocksCreated)).toEqual(
853+
blocks
854+
.map((_) => _)
855+
.reverse()
856+
.splice(0, 10)
857+
);
858+
});
859+
860+
it('asc order', async () => {
861+
const { pageResults } = await provider.queryStakePools(setSortCondition({ pagination }, 'asc', 'blocks'));
862+
863+
expect(pageResults.map(({ metrics }) => metrics!.blocksCreated)).toEqual(
864+
blocks.filter((_) => _ !== null).splice(0, 10)
865+
);
866+
});
867+
});
868+
869+
describe('sort by liveStake', () => {
870+
let pledges: bigint[] = [];
871+
872+
beforeAll(() => {
873+
pledges = poolsInfoWithMetrics
874+
.filter(({ stake }) => stake !== null)
875+
.map(({ stake }) => BigInt(stake))
876+
.sort((a, b) => (a < b ? -1 : 1));
877+
});
878+
879+
it('desc order', async () => {
880+
const { pageResults } = await provider.queryStakePools(
881+
setSortCondition({ pagination }, 'desc', 'liveStake')
882+
);
883+
884+
expect(pageResults.map(({ metrics }) => metrics!.stake.live)).toEqual(
885+
pledges
886+
.map((_) => _)
887+
.reverse()
888+
.splice(0, 10)
889+
);
890+
});
891+
892+
it('asc order', async () => {
893+
const { pageResults } = await provider.queryStakePools(
894+
setSortCondition({ pagination }, 'asc', 'liveStake')
895+
);
896+
897+
expect(pageResults.map(({ metrics }) => metrics!.stake.live)).toEqual(
898+
pledges.map((_) => _).splice(0, 10)
899+
);
900+
});
901+
});
783902
});
784903
});
785904

packages/cardano-services/test/StakePool/TypeormStakePoolProvider/fitxures/TypeormFixtureBuilder.ts

+39-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export type PoolInfo = {
1717
apy?: number;
1818
lastRos?: number;
1919
ros?: number;
20+
pledge: string;
21+
blocks: number;
22+
stake: string;
23+
margin: Cardano.Fraction;
2024
};
2125

2226
export type PoolFixtureModel = {
@@ -29,6 +33,10 @@ export type PoolFixtureModel = {
2933
live_saturation: string;
3034
ros?: string;
3135
last_ros?: string;
36+
pledge: string;
37+
blocks: number;
38+
stake: string;
39+
margin: Cardano.Fraction;
3240
};
3341

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

57-
return result.rows.map(({ id, status, name, ticker, cost, ros, last_ros, live_saturation, metadata_url }) => ({
58-
cost,
59-
id: id as unknown as Cardano.PoolId,
60-
lastRos: typeof last_ros === 'string' ? Number.parseFloat(last_ros) : undefined,
61-
metadataUrl: metadata_url,
62-
name,
63-
ros: typeof ros === 'string' ? Number.parseFloat(ros) : undefined,
64-
saturation: Number.parseFloat(live_saturation),
65-
status,
66-
ticker
67-
}));
65+
return result.rows.map(
66+
({
67+
id,
68+
status,
69+
name,
70+
ticker,
71+
cost,
72+
ros,
73+
last_ros,
74+
live_saturation,
75+
metadata_url,
76+
pledge,
77+
blocks,
78+
stake,
79+
margin
80+
}) => ({
81+
blocks,
82+
cost,
83+
id: id as unknown as Cardano.PoolId,
84+
lastRos: typeof last_ros === 'string' ? Number.parseFloat(last_ros) : undefined,
85+
margin,
86+
metadataUrl: metadata_url,
87+
name,
88+
pledge,
89+
ros: typeof ros === 'string' ? Number.parseFloat(ros) : undefined,
90+
saturation: Number.parseFloat(live_saturation),
91+
stake,
92+
status,
93+
ticker
94+
})
95+
);
6896
}
6997
}

packages/cardano-services/test/StakePool/TypeormStakePoolProvider/fitxures/queries.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ export const withMeta = (withMetadata: boolean) => `AND pr.metadata_url IS ${wit
22

33
// Query returns all pools except delisted
44
export const findStakePools = (hasMetadata = false) => `
5-
SELECT
6-
sp.id, sp.status, pr.reward_account, pr.pledge,
7-
pr.cost, pr.margin,pr.relays, pr.owners, pr.vrf,
8-
pr.metadata_url, pr.metadata_hash, pr.block_slot,
9-
pm.name, pm.ticker, pm.description, pm.homepage,
10-
pm.ext, cpm.live_saturation, cpm.last_ros, cpm.ros
5+
SELECT
6+
sp.id, sp.status, pr.reward_account, pr.pledge,
7+
pr.cost, pr.margin, pr.relays, pr.owners, pr.vrf,
8+
pr.metadata_url, pr.metadata_hash, pr.block_slot,
9+
pm.name, pm.ticker, pm.description, pm.homepage,
10+
pm.ext, cpm.live_saturation, cpm.last_ros, cpm.ros,
11+
cpm.minted_blocks AS blocks, cpm.live_stake AS stake
1112
FROM public.stake_pool as sp
1213
LEFT JOIN pool_registration as pr ON sp.last_registration_id = pr.id
1314
LEFT JOIN pool_metadata as pm ON pr.id = pm.pool_update_id

packages/core/src/Provider/StakePoolProvider/util.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
export const PoolDataSortFields = ['name', 'cost'] as const;
3-
export const PoolMetricsSortFields = ['saturation'] as const;
2+
export const PoolDataSortFields = ['cost', 'name', 'margin', 'pledge'] as const;
3+
export const PoolMetricsSortFields = ['blocks', 'liveStake', 'saturation'] as const;
44
export const PoolAPYSortFields = ['apy'] as const;
55
export const PoolROSSortFields = ['ros', 'lastRos'] as const;
66

0 commit comments

Comments
 (0)