Skip to content

Commit

Permalink
feat: add sort stake pools by saturation
Browse files Browse the repository at this point in the history
refactor: improve stake pool queries to allow adding more sort fields

fix: make stake pool order by name case insensitive and move nulls to last
  • Loading branch information
lgobbi-atix committed Jun 3, 2022
1 parent cd992dd commit 7805dd8
Show file tree
Hide file tree
Showing 15 changed files with 5,120 additions and 2,305 deletions.
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,60 @@ 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();
const [
poolDatas,
poolRelays,
poolOwners,
poolRegistrations,
poolRetirements,
poolRewards,
lastEpoch,
poolMetrics,
totalCount
] = 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)
]);
return toCoreStakePool({

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 stake pool extra information');
const [poolRelays, poolOwners, poolRegistrations, poolRetirements, poolRewards, poolMetrics, totalCount] =
await Promise.all([
// 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.#logger.debug('About to get last epoch');
const lastEpoch = await this.#builder.getLastEpoch();

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 @@ -47,43 +47,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((result) => result !== undefined) as Cardano.StakePool[],
totalResultCount: Number(totalCount)
});

Expand Down Expand Up @@ -142,7 +150,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 @@ -328,13 +329,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 @@ -660,13 +664,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 @@ -752,6 +753,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

0 comments on commit 7805dd8

Please sign in to comment.