All files / src/services/DelegationTracker RewardsHistory.ts

100% Statements 35/35
100% Branches 2/2
100% Functions 14/14
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 9642x 42x     42x       42x 42x 42x   524x 131x   42x 42x 127x           128x 127x       127x             1x 1x         42x             126x   127x   127x     6x       126x               132x 132x 1x 1x               131x           131x               131x      
import { BigIntMath } from '@cardano-sdk/util';
import { Cardano, Reward } from '@cardano-sdk/core';
import { KeyValueStore } from '../../persistence';
import { Logger } from 'ts-log';
import { Observable, concat, map, of, switchMap, tap } from 'rxjs';
import { RetryBackoffConfig } from 'backoff-rxjs';
import { RewardsHistory } from '../types';
import { TrackedRewardsProvider } from '../ProviderTracker';
import { pollProvider } from '../util';
import flatten from 'lodash/flatten.js';
import sortBy from 'lodash/sortBy.js';
 
const sumRewards = (arrayOfRewards: Reward[]) => BigIntMath.sum(arrayOfRewards.map(({ rewards }) => rewards));
const avgReward = (arrayOfRewards: Reward[]) => sumRewards(arrayOfRewards) / BigInt(arrayOfRewards.length);
 
export const createRewardsHistoryProvider =
  (rewardsProvider: TrackedRewardsProvider, retryBackoffConfig: RetryBackoffConfig) =>
  (
    rewardAccounts: Cardano.RewardAccount[],
    lowerBound: Cardano.EpochNo | null,
    epoch$: Observable<Cardano.EpochNo>,
    logger: Logger
  ): Observable<Map<Cardano.RewardAccount, Reward[]>> => {
    if (lowerBound) {
      return pollProvider({
        logger,
        retryBackoffConfig,
        sample: () =>
          rewardsProvider.rewardsHistory({
            epochs: { lowerBound },
            rewardAccounts
          }),
        trigger$: epoch$
      });
    }
    rewardsProvider.setStatInitialized(rewardsProvider.stats.rewardsHistory$);
    return of(new Map());
  };
 
export type RewardsHistoryProvider = ReturnType<typeof createRewardsHistoryProvider>;
 
export const createRewardsHistoryTracker = (
  rewardAccounts$: Observable<Cardano.RewardAccount[]>,
  epoch$: Observable<Cardano.EpochNo>,
  rewardsHistoryProvider: RewardsHistoryProvider,
  rewardsHistoryStore: KeyValueStore<Cardano.RewardAccount, Reward[]>,
  logger: Logger
): Observable<RewardsHistory> =>
  rewardAccounts$
    .pipe(
      tap((rewardsAccounts) => logger.debug(`Fetching rewards for ${rewardsAccounts.length} accounts`)),
      switchMap((rewardAccounts) =>
        concat(
          rewardsHistoryStore
            .getValues(rewardAccounts)
            .pipe(map((rewards) => new Map(rewardAccounts.map((rewardAccount, i) => [rewardAccount, rewards[i]])))),
          // this could be optimized to fetch rewards > last local reward within stability window
          rewardsHistoryProvider(rewardAccounts, Cardano.EpochNo(1), epoch$, logger).pipe(
            tap((allRewards) =>
              rewardsHistoryStore.setAll([...allRewards.entries()].map(([key, value]) => ({ key, value })))
            )
          )
        )
      )
    )
    .pipe(
      map((rewardsByAccount) => {
        const all = sortBy(flatten([...rewardsByAccount.values()]), 'epoch');
        if (all.length === 0) {
          logger.debug('No rewards found');
          return {
            all: [],
            avgReward: null,
            lastReward: null,
            lifetimeRewards: 0n
          } as RewardsHistory;
        }
 
        const rewardsHistory: RewardsHistory = {
          all,
          avgReward: avgReward(all),
          lastReward: all[all.length - 1],
          lifetimeRewards: sumRewards(all)
        };
        logger.debug(
          `Rewards between epochs ${rewardsHistory.all[0].epoch} and ${
            rewardsHistory.all[rewardsHistory.all.length - 1].epoch
          }`,
          `average:${rewardsHistory.avgReward}`,
          `lastRewards:${rewardsHistory.lastReward}`,
          `lifetimeRewards:${rewardsHistory.lifetimeRewards}`
        );
        return rewardsHistory;
      })
    );