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

feat: add sort stake pools by saturation #270

Merged
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
@@ -1,9 +1,10 @@
/* eslint-disable sonarjs/no-nested-template-literals */
import { CommonPoolInfo, PoolData, PoolMetrics, PoolSortType } from './types';
import { DbSyncProvider } from '../../DbSyncProvider';
import { Logger, dummyLogger } from 'ts-log';
import { Pool } from 'pg';
import { StakePoolBuilder } from './StakePoolBuilder';
import { StakePoolProvider, StakePoolQueryOptions, StakePoolSearchResults, StakePoolStats } from '@cardano-sdk/core';
import { getStakePoolSortType } from './util';
import { isNotNil } from '@cardano-sdk/util';
import { toCoreStakePool } from './mappers';

Expand All @@ -17,6 +18,23 @@ export class DbSyncStakePoolProvider extends DbSyncProvider implements StakePool
this.#logger = logger;
}

private getQueryBySortType(
sortType: PoolSortType,
queryArgs: { hashesIds: number[]; updatesIds: number[]; totalAdaAmount: string }
) {
const { hashesIds, updatesIds, totalAdaAmount } = queryArgs;
// Identify which query to use to order and paginate the result
// Should be the only one to get the sort options, rest should be ordered by their own defaults
switch (sortType) {
// Add more cases as more sort types are supported
case 'metrics':
return (options?: StakePoolQueryOptions) => this.#builder.queryPoolMetrics(hashesIds, totalAdaAmount, options);
case 'data':
default:
return (options?: StakePoolQueryOptions) => this.#builder.queryPoolData(updatesIds, options);
}
}

public async queryStakePools(options?: StakePoolQueryOptions): Promise<StakePoolSearchResults> {
const { params, query } =
options?.filters?._condition === 'or'
Expand All @@ -25,31 +43,66 @@ export class DbSyncStakePoolProvider extends DbSyncProvider implements StakePool
this.#logger.debug('About to query pool hashes');
const poolUpdates = await this.#builder.queryPoolHashes(query, params);
const hashesIds = poolUpdates.map(({ id }) => id);
this.#logger.debug(`${hashesIds.length} pools found`);
const updatesIds = poolUpdates.map(({ updateId }) => updateId);
this.#logger.debug(`${hashesIds.length} pools found`);
const totalAdaAmount = await this.#builder.getTotalAmountOfAda();

this.#logger.debug('About to query stake pools by sort options');
const sortType = options?.sort?.field ? getStakePoolSortType(options.sort.field) : 'data';
const orderedResult = await this.getQueryBySortType(sortType, { hashesIds, totalAdaAmount, updatesIds })(options);
const orderedResultHashIds = (orderedResult as CommonPoolInfo[]).map(({ hashId }) => hashId);
const orderedResultUpdateIds = poolUpdates
.filter(({ id }) => orderedResultHashIds.includes(id))
.map(({ updateId }) => updateId);

let poolDatas: PoolData[] = [];
if (sortType !== 'data') {
// If queryPoolData is not the one used to sort there could be more stake pools that should be fetched
// but might not appear in the orderByQuery result
this.#logger.debug('About to query stake pools data');
poolDatas = await this.#builder.queryPoolData(orderedResultUpdateIds);

// If not reached, try to fill the pagination limit using pool data default order
if (options?.pagination?.limit && orderedResult.length < options.pagination.limit) {
const restOfPoolUpdateIds = updatesIds.filter((updateId) => !orderedResultUpdateIds.includes(updateId));
this.#logger.debug('About to query rest of stake pools data');
const restOfPoolData = await this.#builder.queryPoolData(restOfPoolUpdateIds, {
pagination: { limit: options.pagination.limit - orderedResult.length, startAt: 0 }
});
poolDatas.push(...restOfPoolData);
orderedResultUpdateIds.push(...restOfPoolData.map(({ updateId }) => updateId));
orderedResultHashIds.push(...restOfPoolData.map(({ hashId }) => hashId));
}
} else {
poolDatas = orderedResult as PoolData[];
}

this.#logger.debug('About to query stake pool extra information');
const [
poolDatas,
poolRelays,
poolOwners,
poolRegistrations,
poolRetirements,
poolRewards,
lastEpoch,
poolMetrics,
totalCount
totalCount,
lastEpoch
] = await Promise.all([
this.#builder.queryPoolData(updatesIds, options),
this.#builder.queryPoolRelays(updatesIds),
this.#builder.queryPoolOwners(updatesIds),
this.#builder.queryRegistrations(hashesIds),
this.#builder.queryRetirements(hashesIds),
this.#builder.queryPoolRewards(hashesIds, options?.rewardsHistoryLimit),
this.#builder.getLastEpoch(),
this.#builder.queryPoolMetrics(hashesIds, totalAdaAmount),
this.#builder.queryTotalCount(query, params)
// TODO: it would be easier and make the code cleaner if all queries had the same id as argument
// (either hash or update id)
this.#builder.queryPoolRelays(orderedResultUpdateIds),
this.#builder.queryPoolOwners(orderedResultUpdateIds),
this.#builder.queryRegistrations(orderedResultHashIds),
this.#builder.queryRetirements(orderedResultHashIds),
this.#builder.queryPoolRewards(orderedResultHashIds, options?.rewardsHistoryLimit),
sortType === 'metrics'
? (orderedResult as PoolMetrics[])
: this.#builder.queryPoolMetrics(orderedResultHashIds, totalAdaAmount),
this.#builder.queryTotalCount(query, params),
this.#builder.getLastEpoch()
]);
return toCoreStakePool({

return toCoreStakePool(orderedResultHashIds, {
lastEpoch,
poolDatas,
poolMetrics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {
EpochModel,
EpochRewardModel,
OrderByOptions,
OwnerAddressModel,
PoolDataModel,
PoolMetricsModel,
Expand Down Expand Up @@ -90,8 +91,12 @@ export class StakePoolBuilder {
}
public async queryPoolData(updatesIds: number[], options?: StakePoolQueryOptions) {
this.#logger.debug('About to query pool data');
const defaultSort: OrderByOptions[] = [
{ field: 'name', order: 'asc' },
{ field: 'pool_id', order: 'asc' }
];
const queryWithSortAndPagination = withPagination(
withSort(Queries.findPoolsData, options?.sort),
withSort(Queries.findPoolsData, options?.sort, defaultSort),
options?.pagination
);
const result: QueryResult<PoolDataModel> = await this.#db.query(queryWithSortAndPagination, [updatesIds]);
Expand All @@ -102,9 +107,13 @@ export class StakePoolBuilder {
const result: QueryResult<PoolUpdateModel> = await this.#db.query(query, params);
return result.rows.length > 0 ? result.rows.map(mapPoolUpdate) : [];
}
public async queryPoolMetrics(hashesIds: number[], totalAdaAmount: string) {
this.#logger.debug('About to query pool data');
const result: QueryResult<PoolMetricsModel> = await this.#db.query(Queries.findPoolsMetrics, [
public async queryPoolMetrics(hashesIds: number[], totalAdaAmount: string, options?: StakePoolQueryOptions) {
this.#logger.debug('About to query pool metrics');
const queryWithSortAndPagination = withPagination(
withSort(Queries.findPoolsMetrics, options?.sort, [{ field: 'saturation', order: 'desc' }]),
options?.pagination
);
const result: QueryResult<PoolMetricsModel> = await this.#db.query(queryWithSortAndPagination, [
hashesIds,
totalAdaAmount
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
RelayModel,
StakePoolStatsModel
} from './types';
import { isNotNil } from '@cardano-sdk/util';
import Fraction from 'fraction.js';

const toHexString = (bytes: Buffer) => bytes.toString('hex');
Expand Down Expand Up @@ -47,43 +48,51 @@ interface ToCoreStakePoolInput {
totalCount: number;
}

export const toCoreStakePool = ({
poolOwners,
poolDatas,
poolRegistrations,
poolRelays,
poolRetirements,
poolRewards,
lastEpoch,
poolMetrics,
totalCount
}: ToCoreStakePoolInput): StakePoolSearchResults => ({
pageResults: poolDatas.map((poolData) => {
const registrations = poolRegistrations.filter((r) => r.hashId === poolData.hashId);
const retirements = poolRetirements.filter((r) => r.hashId === poolData.hashId);
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),
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),
rewardAccount: poolData.rewardAccount,
status: getPoolStatus(registrations[0], lastEpoch, retirements[0]),
transactions: {
registration: registrations.map((r) => r.transactionId),
retirement: retirements.map((r) => r.transactionId)
},
vrf: poolData.vrfKeyHash
};
if (poolData.metadata) toReturn.metadata = poolData.metadata;
if (poolData.metadataJson) toReturn.metadataJson = poolData.metadataJson;
return toReturn;
}),
export const toCoreStakePool = (
poolHashIds: number[],
{
poolOwners,
poolDatas,
poolRegistrations,
poolRelays,
poolRetirements,
poolRewards,
lastEpoch,
poolMetrics,
totalCount
}: ToCoreStakePoolInput
): StakePoolSearchResults => ({
pageResults: poolHashIds
.map((hashId) => {
const poolData = poolDatas.find((data) => data.hashId === hashId);
if (!poolData) return;
const registrations = poolRegistrations.filter((r) => r.hashId === poolData.hashId);
const retirements = poolRetirements.filter((r) => r.hashId === poolData.hashId);
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),
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),
rewardAccount: poolData.rewardAccount,
status: getPoolStatus(registrations[0], lastEpoch, retirements[0]),
transactions: {
registration: registrations.map((r) => r.transactionId),
retirement: retirements.map((r) => r.transactionId)
},
vrf: poolData.vrfKeyHash
};
if (poolData.metadata) toReturn.metadata = poolData.metadata;
if (poolData.metadataJson) toReturn.metadataJson = poolData.metadataJson;
return toReturn;
})
.filter(isNotNil),
totalResultCount: Number(totalCount)
});

Expand Down Expand Up @@ -142,7 +151,7 @@ export const mapRelay = (relayModel: RelayModel): PoolRelay => {
port: relayModel.port
};

return { relay, updateId: relayModel.update_id };
return { hashId: relayModel.hash_id, relay, updateId: relayModel.update_id };
};

export const mapEpochReward = (epochRewardModel: EpochRewardModel, hashId: number): EpochReward => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/no-nested-template-literals */
import { Cardano, MultipleChoiceSearchFilter, StakePoolQueryOptions } from '@cardano-sdk/core';
import { SubQuery } from './types';
import { OrderByOptions, SubQuery } from './types';
import { getStakePoolSortType } from './util';

export const findLastEpoch = `
SELECT
Expand Down Expand Up @@ -348,13 +349,16 @@ JOIN pool_update pu

export const findPoolsRelays = `
SELECT
update_id
hash_id,
update_id,
ipv4,
ipv6,
port,
dns_name,
dns_srv_name AS hostname --fixme: check this is correct
FROM pool_relay
JOIN pool_update
ON pool_relay.update_id = pool_update.id
WHERE update_id = ANY($1)
`;

Expand Down Expand Up @@ -680,13 +684,10 @@ export const withPagination = (query: string, pagination?: StakePoolQueryOptions
return query;
};

export const defaultSort: StakePoolQueryOptions['sort'] = {
field: 'name',
order: 'asc'
};

export const withSort = (query: string, sort: StakePoolQueryOptions['sort'] = defaultSort) =>
`${query} ORDER BY ${sort.field} ${sort.order}, pool_id ASC`;
const orderBy = (query: string, sort: OrderByOptions[]) =>
sort && sort.length > 0
? `${query} ORDER BY ${sort.map(({ field, order }) => `${field} ${order} NULLS LAST`).join(', ')}`
: query;

export const addSentenceToQuery = (query: string, sentence: string) => query + sentence;

Expand Down Expand Up @@ -772,6 +773,28 @@ FROM last_pool_update AS pool_update
LEFT JOIN last_pool_retire AS pool_retire
ON pool_update.hash_id = pool_retire.hash_id`;

const sortFieldMapping: Record<string, string> = {
name: "lower((pod.json -> 'name')::TEXT)"
};

export const withSort = (query: string, sort?: StakePoolQueryOptions['sort'], defaultSort?: OrderByOptions[]) => {
if (!sort?.field && defaultSort) {
const defaultMappedSort = defaultSort.map((s) => ({ field: sortFieldMapping[s.field] || s.field, order: s.order }));
return orderBy(query, defaultMappedSort);
}
if (!sort?.field) return query;
const sortType = getStakePoolSortType(sort.field);
const mappedSort = { field: sortFieldMapping[sort.field] || sort.field, order: sort.order };
switch (sortType) {
case 'data':
return orderBy(query, [mappedSort, { field: 'pool_id', order: 'asc' }]);
case 'metrics':
return orderBy(query, [mappedSort, { field: 'id', order: 'asc' }]);
default:
return orderBy(query, [mappedSort]);
}
};

const Queries = {
IDENTIFIER_QUERY,
POOLS_WITH_PLEDGE_MET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ export interface PoolUpdate {
updateId: number;
}

export interface PoolData {
export interface CommonPoolInfo {
hashId: number;
}

export interface PoolData extends CommonPoolInfo {
hexId: Cardano.PoolIdHex;
id: Cardano.PoolId;
rewardAccount: Cardano.RewardAccount;
Expand Down Expand Up @@ -40,6 +43,7 @@ export interface PoolDataModel {
}

export interface RelayModel {
hash_id: number;
update_id: number;
ipv4?: string;
ipv6?: string;
Expand Down Expand Up @@ -76,17 +80,15 @@ interface PoolTransactionModel {
hash_id: number;
}

interface PoolTransaction {
hashId: number;
interface PoolTransaction extends CommonPoolInfo {
transactionId: Cardano.TransactionId;
}

export interface PoolOwner {
export interface PoolOwner extends CommonPoolInfo {
address: Cardano.RewardAccount;
hashId: number;
}

export interface PoolRelay {
export interface PoolRelay extends CommonPoolInfo {
relay: Cardano.Relay;
updateId: number;
}
Expand Down Expand Up @@ -128,8 +130,7 @@ export interface PoolMetricsModel {
pool_hash_id: number;
}

export interface PoolMetrics {
hashId: number;
export interface PoolMetrics extends CommonPoolInfo {
metrics: Cardano.StakePoolMetrics;
}

Expand All @@ -142,3 +143,9 @@ export interface StakePoolStatsModel {
retired: string;
retiring: string;
}

export type PoolSortType = 'data' | 'metrics';
export interface OrderByOptions {
field: string;
order: 'asc' | 'desc';
}
Loading