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-10165 Stake pool advanced filters #1230

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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 @@ -93,6 +93,7 @@ export const getWhereClauseAndArgs = (filters: QueryStakePoolsArgs['filters'], t
args = { ...args, status: filters.status };
clauses.push('pool.status IN (:...status)');
}

if (!textFilter && filters.identifier && filters.identifier.values.length > 0) {
const identifierClauses: string[] = [];
const identifierCondition = getFilterCondition(filters.identifier?._condition, 'OR');
Expand Down Expand Up @@ -123,6 +124,7 @@ export const getWhereClauseAndArgs = (filters: QueryStakePoolsArgs['filters'], t
const identifierFilters = identifierClauses.join(` ${identifierCondition} `);
clauses.push(` (${identifierFilters}) `);
}

if (filters.pledgeMet !== undefined && filters.pledgeMet !== null) {
if (filters.pledgeMet) {
clauses.push('params.pledge<=metrics.live_pledge');
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Cardano/types/StakePool/StakePool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PoolParameters } from './PoolParameters';

/** Stake quantities for a Stake Pool. */
export interface StakePoolMetricsStake {
/** The total amount of stake currently delegated to the pool. This will be snapshotted at the end of the epoch. */
/** The total amount of stake currently delegated to the pool. A snapshot will be taken at the end of the epoch. */
live: Lovelace;

/** A snapshot from 2 epochs ago, used in the current epoch as part of the block leadership schedule. */
Expand All @@ -15,7 +15,7 @@ export interface StakePoolMetricsStake {

/** Stake percentages for a Stake Pool. */
export interface StakePoolMetricsSize {
/** The percentage of stake currently delegated to the pool. This will be snapshotted at the end of the epoch. */
/** The percentage of stake currently delegated to the pool. A snapshot will be taken at the end of the epoch. */
live: Percent;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ export interface FuzzyOptions {
weights: { description: number; homepage: number; name: number; poolId: number; ticker: number };
}

export const MetricsFiltersFields = [
'blocks',
'cost',
'lastRos',
'margin',
'pledge',
'ros',
'saturation',
'stake'
] as const;
export type MetricsFiltersFields = typeof MetricsFiltersFields[number];
export type MetricsFiltersFieldType<F extends MetricsFiltersFields> = F extends 'pledge' | 'stake'
? Cardano.Lovelace
: number;
export type MetricsFilters = {
[K in MetricsFiltersFields]?: { from?: MetricsFiltersFieldType<K>; to?: MetricsFiltersFieldType<K> };
};

/** The StakePoolProvider.queryStakePools call arguments. */
export interface QueryStakePoolsArgs {
/** Will return all stake pools sorted by name ascending if not specified. */
Expand All @@ -51,6 +69,9 @@ export interface QueryStakePoolsArgs {
/** Will return results for partial matches. */
identifier?: MultipleChoiceSearchFilter<FilterIdentifiers>;

/** Range filters on some selected metrics fields. */
metrics?: MetricsFilters;

/** If provided, returns all the pools which live stake meets / do not meets the pledge. */
pledgeMet?: boolean;

Expand Down
63 changes: 63 additions & 0 deletions packages/core/src/Provider/StakePoolProvider/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MetricsFilters, MetricsFiltersFieldType, MetricsFiltersFields } from './types';

/* eslint-disable @typescript-eslint/no-explicit-any */
export const PoolDataSortFields = ['cost', 'name', 'margin', 'pledge', 'ticker'] as const;
export const PoolMetricsSortFields = ['blocks', 'liveStake', 'saturation'] as const;
Expand All @@ -10,3 +12,64 @@ export const isPoolAPYSortField = (value: string) => PoolAPYSortFields.includes(
export const isPoolROSSortField = (value: string) => PoolROSSortFields.includes(value as any);

export const SortFields = [...PoolDataSortFields, ...PoolMetricsSortFields, ...PoolAPYSortFields, ...PoolROSSortFields];

export const metricsFilterBoundaries: {
[K in MetricsFiltersFields]: { lower: MetricsFiltersFieldType<K>; upper: MetricsFiltersFieldType<K> };
} = {
blocks: { lower: 0, upper: 100_000 },
cost: { lower: 170, upper: 1_000_000 },
lastRos: { lower: 0, upper: 1 },
margin: { lower: 0, upper: 1 },
pledge: { lower: 0n, upper: 80_000_000_000_000n },
ros: { lower: 0, upper: 1 },
saturation: { lower: 0, upper: 1.5 },
stake: { lower: 0n, upper: 80_000_000_000_000n }
};

export class MetricsFilterError<T extends MetricsFiltersFields> extends RangeError {
constructor(
public field: T,
public subField: 'from' | 'to',
public check: 'lower' | 'order' | 'upper',
value: Exclude<MetricsFilters[T], undefined>
) {
const message = (() => {
switch (check) {
case 'lower':
return `lesser than lower boundary ${metricsFilterBoundaries[field].lower}`;
case 'order':
return `greater than ${field}.to ${value.to}`;
case 'upper':
return `greater than upper boundary ${metricsFilterBoundaries[field].upper}`;
}
})();

super(`${field}.${subField} ${value[subField]} ${message}`);
}
}

export const checkMetricsFiltersField = <T extends MetricsFiltersFields>(
field: T,
value: Exclude<MetricsFilters[T], undefined>
) => {
if (value.from !== undefined) {
if (value.from < metricsFilterBoundaries[field].lower) throw new MetricsFilterError(field, 'from', 'lower', value);
if (value.from > metricsFilterBoundaries[field].upper) throw new MetricsFilterError(field, 'from', 'upper', value);
if (value.to !== undefined && value.from > value.to) throw new MetricsFilterError(field, 'from', 'order', value);
}

if (value.to !== undefined) {
if (value.to < metricsFilterBoundaries[field].lower) throw new MetricsFilterError(field, 'to', 'lower', value);
if (value.to > metricsFilterBoundaries[field].upper) throw new MetricsFilterError(field, 'to', 'upper', value);
}
};

export const checkMetricsFilters = (metrics?: MetricsFilters) => {
if (!metrics) return;

for (const field of MetricsFiltersFields) {
const filter = metrics[field];

if (filter) checkMetricsFiltersField(field, filter);
}
};
52 changes: 52 additions & 0 deletions packages/core/test/Provider/StakePoolProvider/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MetricsFilterError, MetricsFilters, MetricsFiltersFields, checkMetricsFilters } from '../../../src';

const testError = (
filters: MetricsFilters,
message: string,
field: MetricsFiltersFields,
subField: 'from' | 'to',
check: 'lower' | 'order' | 'upper'
) => {
expect.assertions(5);

try {
checkMetricsFilters(filters);
} catch (error) {
expect(error).toBeInstanceOf(MetricsFilterError);

if (error instanceof MetricsFilterError) {
expect(error.message).toEqual(message);
expect(error.field).toEqual(field);
expect(error.subField).toEqual(subField);
expect(error.check).toEqual(check);
}
}
};

describe('checkMetricsFilters', () => {
it("doesn't throw with value inside boundaries", () => {
expect.assertions(0);
checkMetricsFilters({ cost: { from: 200, to: 2000 }, margin: { from: 0.5 }, pledge: { to: 1_000_000n } });
});

it('throws with ros.from lower than lower boundary', () =>
testError({ ros: { from: -2 } }, 'ros.from -2 lesser than lower boundary 0', 'ros', 'from', 'lower'));

it('throws with stake.to greater than upper boundary', () =>
testError(
{ stake: { to: 100_000_000_000_000n } },
'stake.to 100000000000000 greater than upper boundary 80000000000000',
'stake',
'to',
'upper'
));

it('throws with blocks.from greater than blocks.to', () =>
testError(
{ blocks: { from: 1000, to: 100 } },
'blocks.from 1000 greater than blocks.to 100',
'blocks',
'from',
'order'
));
});
Loading