Skip to content

Commit

Permalink
feat!: epoch rewards now includes the pool id of the pool that genera…
Browse files Browse the repository at this point in the history
…ted the reward

BREAKING CHANGE: EpochRewards renamed to Reward
- The pool the stake address was delegated to when the reward is earned is now
included in the EpochRewards (Will be null for payments from the treasury or the reserves)
- Reward no longer coalesce rewards from the same epoch
  • Loading branch information
AngelCastilloB committed Aug 10, 2023
1 parent 8ece0b9 commit 96fd72b
Show file tree
Hide file tree
Showing 14 changed files with 72 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cardano, EpochRewards } from '@cardano-sdk/core';
import { Cardano, Reward } from '@cardano-sdk/core';
import { healthCheckResponseWithState } from '../util';
import { logger } from '@cardano-sdk/util-dev';
import { rewardsHttpProvider } from '../../src';
Expand Down Expand Up @@ -43,7 +43,7 @@ describe('rewardsHttpProvider', () => {
expect(response).toEqual(expectedResponse);
});
test('rewardsHistory doesnt throw', async () => {
const expectedResponse = toSerializableObject(new Map<Cardano.RewardAccount, EpochRewards[]>());
const expectedResponse = toSerializableObject(new Map<Cardano.RewardAccount, Reward[]>());
axiosMock.onPost().replyOnce(200, expectedResponse);
const provider = rewardsHttpProvider(config);
const response = toSerializableObject(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Cardano, EpochRewards } from '@cardano-sdk/core';
import { Cardano, Reward } from '@cardano-sdk/core';
import { RewardEpochModel } from './types';

export const rewardsToCore = (rewards: RewardEpochModel[]): Map<Cardano.RewardAccount, EpochRewards[]> =>
export const rewardsToCore = (rewards: RewardEpochModel[]): Map<Cardano.RewardAccount, Reward[]> =>
rewards.reduce((_rewards, current) => {
const coreReward = current.address as unknown as Cardano.RewardAccount;
const poolId = current.pool_id ? (current.pool_id as unknown as Cardano.PoolId) : undefined;
const epochRewards = _rewards.get(coreReward);
const currentEpochReward = { epoch: Cardano.EpochNo(current.epoch), rewards: BigInt(current.quantity) };
const currentEpochReward = {
epoch: Cardano.EpochNo(current.epoch),
poolId,
rewards: BigInt(current.quantity)
};
if (epochRewards) {
_rewards.set(coreReward, [...epochRewards, currentEpochReward]);
} else {
_rewards.set(coreReward, [currentEpochReward]);
}
return _rewards;
}, new Map<Cardano.RewardAccount, EpochRewards[]>());
}, new Map<Cardano.RewardAccount, Reward[]>());
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ export const findRewardsHistory = (lowerBound?: number, upperBound?: number) =>
SELECT
SUM(r.amount) AS quantity,
sa."view" AS address,
r.earned_epoch AS epoch
r.earned_epoch AS epoch,
ph."view" as pool_id
FROM reward r
JOIN stake_address sa ON
sa.id = r.addr_id AND
sa."view" = ANY($1)
JOIN epochs ON
r.earned_epoch = epochs.epoch_no
GROUP BY sa."view", r.earned_epoch
JOIN pool_hash ph ON
r.pool_id = ph.id
GROUP BY sa."view", r.earned_epoch, ph."view"
ORDER BY quantity ASC
`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface RewardEpochModel {
quantity: string;
address: string;
epoch: number;
pool_id?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('DbSyncRewardProvider mappers', () => {
{
address: rewardAddress1,
epoch: 10,
pool_id: 'pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf',
quantity: '10000000'
}
];
Expand All @@ -25,11 +26,13 @@ describe('DbSyncRewardProvider mappers', () => {
{
address: rewardAddress1,
epoch: 11,
pool_id: 'pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf',
quantity: '20000000'
},
{
address: 'stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27',
epoch: 15,
pool_id: 'pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf',
quantity: '30000000'
}
];
Expand All @@ -38,9 +41,21 @@ describe('DbSyncRewardProvider mappers', () => {

expect(result.size).toEqual(2);
expect(result.has(Cardano.RewardAccount(rewardAddress1))).toEqual(true);
expect(result.get(Cardano.RewardAccount(rewardAddress1))![0]).toEqual({ epoch: 10, rewards: 10_000_000n });
expect(result.get(Cardano.RewardAccount(rewardAddress1))![1]).toEqual({ epoch: 11, rewards: 20_000_000n });
expect(result.get(Cardano.RewardAccount(rewardAddress1))![0]).toEqual({
epoch: 10,
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 10_000_000n
});
expect(result.get(Cardano.RewardAccount(rewardAddress1))![1]).toEqual({
epoch: 11,
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 20_000_000n
});
expect(result.has(Cardano.RewardAccount(rewardAddress2))).toEqual(true);
expect(result.get(Cardano.RewardAccount(rewardAddress2))![0]).toEqual({ epoch: 15, rewards: 30_000_000n });
expect(result.get(Cardano.RewardAccount(rewardAddress2))![0]).toEqual({
epoch: 15,
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 30_000_000n
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ describe('RewardsHttpService', () => {
rewardAccounts: [rewardAccount]
});
expect(response.get(rewardAccount)!.length).toBeGreaterThan(0);
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', rewards: 0n });
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', poolId: 'pool_id', rewards: 0n });
});

it('returns no rewards address history for empty reward accounts', async () => {
Expand Down Expand Up @@ -277,7 +277,7 @@ describe('RewardsHttpService', () => {

expect(lowestEpoch).toBeGreaterThanOrEqual(1);
expect(highestEpoch).toBeLessThanOrEqual(2);
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', rewards: 0n });
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', poolId: 'pool_id', rewards: 0n });
});

it('returns rewards address history of the epochs filtered', async () => {
Expand All @@ -296,7 +296,7 @@ describe('RewardsHttpService', () => {
}

expect(lowestEpoch).toBeGreaterThanOrEqual(1);
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', rewards: 0n });
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', poolId: 'pool_id', rewards: 0n });
});

it('returns rewards address history some of the epochs filter', async () => {
Expand All @@ -315,7 +315,7 @@ describe('RewardsHttpService', () => {
}

expect(highestEpoch).toBeLessThanOrEqual(10);
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', rewards: 0n });
expect(response.get(rewardAccount)![0]).toMatchShapeOf({ epoch: '0', poolId: 'pool_id', rewards: 0n });
});
});
});
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/Provider/RewardsProvider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { Cardano } from '../..';
import { Provider } from '../Provider';
import { Range } from '@cardano-sdk/util';

export interface EpochRewards {
export interface Reward {
epoch: Cardano.EpochNo;
rewards: Cardano.Lovelace;
/**
* The pool the stake address was delegated to when the reward is earned.
* Will be undefined for payments from the treasury or the reserves.
*/
poolId?: Cardano.PoolId;
}

export interface RewardsHistoryArgs {
Expand All @@ -21,6 +26,6 @@ export interface RewardsProvider extends Provider {
*
* @returns Rewards quantity for every epoch that had any rewards in ascending order.
*/
rewardsHistory: (args: RewardsHistoryArgs) => Promise<Map<Cardano.RewardAccount, EpochRewards[]>>;
rewardsHistory: (args: RewardsHistoryArgs) => Promise<Map<Cardano.RewardAccount, Reward[]>>;
rewardAccountBalance: (args: RewardAccountBalanceArgs) => Promise<Cardano.Lovelace>;
}
9 changes: 6 additions & 3 deletions packages/util-dev/src/mockProviders/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as AssetId from '../assetId';
import { Cardano, EpochRewards, Seconds } from '@cardano-sdk/core';
import { Cardano, Reward, Seconds } from '@cardano-sdk/core';

export const rewardAccount = Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d');
export const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount);
Expand Down Expand Up @@ -42,14 +42,16 @@ export const protocolParameters = {
export const epochRewards = [
{
epoch: Cardano.EpochNo(currentEpoch.number - 3),
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 10_000n
},
{
epoch: Cardano.EpochNo(currentEpoch.number - 2),
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 11_000n
}
];
export const rewardsHistory: Map<Cardano.RewardAccount, EpochRewards[]> = new Map([[rewardAccount, epochRewards]]);
export const rewardsHistory: Map<Cardano.RewardAccount, Reward[]> = new Map([[rewardAccount, epochRewards]]);

export const genesisParameters: Cardano.CompactGenesis = {
activeSlotsCoefficient: 0.05,
Expand All @@ -65,12 +67,13 @@ export const genesisParameters: Cardano.CompactGenesis = {
updateQuorum: 5
};

export const rewardsHistory2 = new Map<Cardano.RewardAccount, EpochRewards[]>([
export const rewardsHistory2 = new Map<Cardano.RewardAccount, Reward[]>([
[
rewardAccount,
[
{
epoch: Cardano.EpochNo(currentEpoch.number - 4),
poolId: Cardano.PoolId('pool1h8yl5mkyrfmfls2x9fu9mls3ry6egnw4q6efg34xr37zc243gkf'),
rewards: 10_000n
},
...epochRewards
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Assets } from '../../types';
import { Cardano, EpochRewards, EraSummary } from '@cardano-sdk/core';
import { Cardano, EraSummary, Reward } from '@cardano-sdk/core';
import { EMPTY, combineLatest, map } from 'rxjs';
import { GroupedAddress } from '@cardano-sdk/key-management';
import { InMemoryCollectionStore } from './InMemoryCollectionStore';
Expand All @@ -22,7 +22,7 @@ export class InMemoryTransactionsStore extends InMemoryCollectionStore<Cardano.H
export class InMemoryUtxoStore extends InMemoryCollectionStore<Cardano.Utxo> {}
export class InMemoryUnspendableUtxoStore extends InMemoryCollectionStore<Cardano.Utxo> {}

export class InMemoryRewardsHistoryStore extends InMemoryKeyValueStore<Cardano.RewardAccount, EpochRewards[]> {}
export class InMemoryRewardsHistoryStore extends InMemoryKeyValueStore<Cardano.RewardAccount, Reward[]> {}
export class InMemoryStakePoolsStore extends InMemoryKeyValueStore<Cardano.PoolId, Cardano.StakePool> {}
export class InMemoryRewardsBalancesStore extends InMemoryKeyValueStore<Cardano.RewardAccount, Cardano.Lovelace> {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Assets } from '../../types';
import { Cardano, EpochRewards, EraSummary } from '@cardano-sdk/core';
import { Cardano, EraSummary, Reward } from '@cardano-sdk/core';
import { CreatePouchDbStoresDependencies } from './types';
import { EMPTY, combineLatest, map } from 'rxjs';
import { GroupedAddress } from '@cardano-sdk/key-management';
Expand All @@ -22,7 +22,7 @@ export class PouchDbVolatileTransactionsStore extends PouchDbDocumentStore<Outgo
export class PouchDbTransactionsStore extends PouchDbCollectionStore<Cardano.HydratedTx> {}
export class PouchDbUtxoStore extends PouchDbCollectionStore<Cardano.Utxo> {}

export class PouchDbRewardsHistoryStore extends PouchDbKeyValueStore<Cardano.RewardAccount, EpochRewards[]> {}
export class PouchDbRewardsHistoryStore extends PouchDbKeyValueStore<Cardano.RewardAccount, Reward[]> {}
export class PouchDbStakePoolsStore extends PouchDbKeyValueStore<Cardano.PoolId, Cardano.StakePool> {}
export class PouchDbRewardsBalancesStore extends PouchDbKeyValueStore<Cardano.RewardAccount, Cardano.Lovelace> {}

Expand Down
4 changes: 2 additions & 2 deletions packages/wallet/src/persistence/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Assets } from '../types';
import { Cardano, EpochRewards, EraSummary, StakeSummary, SupplySummary } from '@cardano-sdk/core';
import { Cardano, EraSummary, Reward, StakeSummary, SupplySummary } from '@cardano-sdk/core';
import { GroupedAddress } from '@cardano-sdk/key-management';
import { Observable } from 'rxjs';
import { OutgoingOnChainTx, TxInFlight } from '../services';
Expand Down Expand Up @@ -76,7 +76,7 @@ export interface WalletStores extends Destroyable {
transactions: OrderedCollectionStore<Cardano.HydratedTx>;
inFlightTransactions: DocumentStore<TxInFlight[]>;
volatileTransactions: DocumentStore<OutgoingOnChainTx[]>;
rewardsHistory: KeyValueStore<Cardano.RewardAccount, EpochRewards[]>;
rewardsHistory: KeyValueStore<Cardano.RewardAccount, Reward[]>;
rewardsBalances: KeyValueStore<Cardano.RewardAccount, Cardano.Lovelace>;
stakePools: KeyValueStore<Cardano.PoolId, Cardano.StakePool>;
protocolParameters: DocumentStore<Cardano.ProtocolParameters>;
Expand Down
29 changes: 11 additions & 18 deletions packages/wallet/src/services/DelegationTracker/RewardsHistory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BigIntMath, isNotNil } from '@cardano-sdk/util';
import { Cardano, EpochRewards, createTxInspector, signedCertificatesInspector } from '@cardano-sdk/core';
import { Cardano, Reward, createTxInspector, signedCertificatesInspector } from '@cardano-sdk/core';
import { KeyValueStore } from '../../persistence';
import { Logger } from 'ts-log';
import { Observable, concat, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs';
Expand All @@ -10,22 +10,22 @@ import { TxWithEpoch } from './types';
import { coldObservableProvider } from '@cardano-sdk/util-rxjs';
import first from 'lodash/first';
import flatten from 'lodash/flatten';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';

const DELEGATION_EPOCHS_AHEAD_COUNT = 2;

export const calcFirstDelegationEpoch = (epoch: Cardano.EpochNo): number => epoch + DELEGATION_EPOCHS_AHEAD_COUNT;

const sumRewards = (arrayOfRewards: EpochRewards[]) => BigIntMath.sum(arrayOfRewards.map(({ rewards }) => rewards));
const avgReward = (arrayOfRewards: EpochRewards[]) => sumRewards(arrayOfRewards) / BigInt(arrayOfRewards.length);
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,
onFatalError?: (value: unknown) => void
): Observable<Map<Cardano.RewardAccount, EpochRewards[]>> => {
): Observable<Map<Cardano.RewardAccount, Reward[]>> => {
if (lowerBound) {
return coldObservableProvider({
onFatalError,
Expand Down Expand Up @@ -63,7 +63,7 @@ export const createRewardsHistoryTracker = (
transactions$: Observable<TxWithEpoch[]>,
rewardAccounts$: Observable<Cardano.RewardAccount[]>,
rewardsHistoryProvider: RewardsHistoryProvider,
rewardsHistoryStore: KeyValueStore<Cardano.RewardAccount, EpochRewards[]>,
rewardsHistoryStore: KeyValueStore<Cardano.RewardAccount, Reward[]>,
logger: Logger,
onFatalError?: (value: unknown) => void
// eslint-disable-next-line max-params
Expand All @@ -90,8 +90,8 @@ export const createRewardsHistoryTracker = (
)
.pipe(
map((rewardsByAccount) => {
const allRewards = flatten([...rewardsByAccount.values()]);
if (allRewards.length === 0) {
const all = sortBy(flatten([...rewardsByAccount.values()]), 'epoch');
if (all.length === 0) {
logger.debug('No rewards found');
return {
all: [],
Expand All @@ -100,19 +100,12 @@ export const createRewardsHistoryTracker = (
lifetimeRewards: 0n
} as RewardsHistory;
}
const rewardsByEpoch = groupBy(allRewards, ({ epoch }) => epoch);
const epochs = Object.keys(rewardsByEpoch)
.map((epoch) => Number(epoch))
.sort();
const all = epochs.map((epoch) => ({
epoch: Cardano.EpochNo(epoch),
rewards: sumRewards(rewardsByEpoch[epoch])
}));

const rewardsHistory: RewardsHistory = {
all,
avgReward: avgReward(allRewards),
avgReward: avgReward(all),
lastReward: all[all.length - 1],
lifetimeRewards: sumRewards(allRewards)
lifetimeRewards: sumRewards(all)
};
logger.debug(
`Rewards between epochs ${rewardsHistory.all[0].epoch} and ${
Expand Down
6 changes: 3 additions & 3 deletions packages/wallet/src/services/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncKeyAgent, GroupedAddress } from '@cardano-sdk/key-management';
import { Cardano, CardanoNodeErrors, EpochRewards, TxCBOR } from '@cardano-sdk/core';
import { Cardano, CardanoNodeErrors, Reward, TxCBOR } from '@cardano-sdk/core';
import { Observable } from 'rxjs';
import { Percent } from '@cardano-sdk/util';
import { SignedTx } from '@cardano-sdk/tx-construction';
Expand Down Expand Up @@ -95,8 +95,8 @@ export interface TransactionsTracker {
}

export interface RewardsHistory {
all: EpochRewards[];
lastReward: EpochRewards | null;
all: Reward[];
lastReward: Reward | null;
avgReward: Cardano.Lovelace | null;
lifetimeRewards: Cardano.Lovelace;
}
Expand Down
1 change: 0 additions & 1 deletion yarn-project.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,6 @@ cacheEntries = {
"fs-minipass@npm:3.0.0" = { filename = "fs-minipass-npm-3.0.0-3692c14b65-b72e9fe426.zip"; sha512 = "b72e9fe426e39f05b35bf237c8218b7ab3f68a65f325725ad7b4e431ff5a10725946fc62883b78446c07515ab938d25fdde3d08fb5ac8693f7f9eb9990da21f0"; };
"fs.realpath@npm:1.0.0" = { filename = "fs.realpath-npm-1.0.0-c8f05d8126-99ddea01a7.zip"; sha512 = "99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0"; };
"fsevents@npm:2.3.2" = { filename = "fsevents-npm-2.3.2-a881d6ac9f-97ade64e75.zip"; sha512 = "97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f"; };
"fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7" = { filename = "fsevents-patch-3340e2eb10-8.zip"; sha512 = "edbd0fd80be379c14409605f77e52fdc78a119e17f875e8b90a220c3e5b29e54a1477c21d91fd30b957ea4866406dc3ff87b61432d2840ff8866b309e5866140"; };
"ftp@npm:0.3.10" = { filename = "ftp-npm-0.3.10-348fb9ac23-ddd313c1d4.zip"; sha512 = "ddd313c1d44eb7429f3a7d77a0155dc8fe86a4c64dca58f395632333ce4b4e74c61413c6e0ef66ea3f3d32d905952fbb6d028c7117d522f793eb1fa282e17357"; };
"function-bind@npm:1.1.1" = { filename = "function-bind-npm-1.1.1-b56b322ae9-b32fbaebb3.zip"; sha512 = "b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a"; };
"function.prototype.name@npm:1.1.5" = { filename = "function.prototype.name-npm-1.1.5-e776a642bb-acd21d733a.zip"; sha512 = "acd21d733a9b649c2c442f067567743214af5fa248dbeee69d8278ce7df3329ea5abac572be9f7470b4ec1cd4d8f1040e3c5caccf98ebf2bf861a0deab735c27"; };
Expand Down

0 comments on commit 96fd72b

Please sign in to comment.