Skip to content

Commit

Permalink
refactor: move rewards methods from Wallet Provider to Rewards Provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Juan Cruz committed May 20, 2022
1 parent 659af71 commit 3a0a2f4
Show file tree
Hide file tree
Showing 29 changed files with 499 additions and 259 deletions.
67 changes: 67 additions & 0 deletions packages/blockfrost/src/blockfrostRewardsProvider.ts
@@ -0,0 +1,67 @@
import { BlockFrostAPI } from '@blockfrost/blockfrost-js';
import { Cardano, EpochRange, EpochRewards, ProviderError, ProviderFailure, RewardsProvider } from '@cardano-sdk/core';
import { formatBlockfrostError } from './util';
/**
* Connect to the [Blockfrost service](https://docs.blockfrost.io/)
*
* @param {BlockFrostAPI} blockfrost BlockFrostAPI instance
* @returns {RewardsProvider} RewardsProvider
* @throws {Cardano.TxSubmissionErrors.UnknownTxSubmissionError}
*/
export const blockfrostRewardsProvider = (blockfrost: BlockFrostAPI): RewardsProvider => {
const healthCheck: RewardsProvider['healthCheck'] = async () => {
try {
const result = await blockfrost.health();
return { ok: result.is_healthy };
} catch (error) {
throw new ProviderError(ProviderFailure.Unknown, error);
}
};

const rewardAccountBalance: RewardsProvider['rewardAccountBalance'] = async (
rewardAccount: Cardano.RewardAccount
) => {
try {
const accountResponse = await blockfrost.accounts(rewardAccount.toString());
return BigInt(accountResponse.withdrawable_amount);
} catch (error) {
if (formatBlockfrostError(error).status_code === 404) {
return 0n;
}
throw error;
}
};
const accountRewards = async (
stakeAddress: Cardano.RewardAccount,
{ lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER }: EpochRange = {}
): Promise<EpochRewards[]> => {
const result: EpochRewards[] = [];
const batchSize = 100;
let page = 1;
let haveMorePages = true;
while (haveMorePages) {
const rewardsPage = await blockfrost.accountsRewards(stakeAddress.toString(), { count: batchSize, page });
result.push(
...rewardsPage
.filter(({ epoch }) => lowerBound <= epoch && epoch <= upperBound)
.map(({ epoch, amount }) => ({
epoch,
rewards: BigInt(amount)
}))
);
haveMorePages = rewardsPage.length === batchSize && rewardsPage[rewardsPage.length - 1].epoch < upperBound;
page += 1;
}
return result;
};
const rewardsHistory: RewardsProvider['rewardsHistory'] = async ({ rewardAccounts, epochs }) => {
const allAddressRewards = await Promise.all(rewardAccounts.map((address) => accountRewards(address, epochs)));
return new Map(allAddressRewards.map((epochRewards, i) => [rewardAccounts[i], epochRewards]));
};

return {
healthCheck,
rewardAccountBalance,
rewardsHistory
};
};
45 changes: 0 additions & 45 deletions packages/blockfrost/src/blockfrostWalletProvider.ts
Expand Up @@ -2,8 +2,6 @@ import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
import { BlockfrostToCore, BlockfrostTransactionContent, BlockfrostUtxo } from './BlockfrostToCore';
import {
Cardano,
EpochRange,
EpochRewards,
ProtocolParametersRequiredByWallet,
ProviderError,
ProviderFailure,
Expand Down Expand Up @@ -86,18 +84,6 @@ export const blockfrostWalletProvider = (blockfrost: BlockFrostAPI, logger = dum
return utxoResults.flat(1);
};

const rewards: WalletProvider['rewardAccountBalance'] = async (rewardAccount: Cardano.RewardAccount) => {
try {
const accountResponse = await blockfrost.accounts(rewardAccount.toString());
return BigInt(accountResponse.withdrawable_amount);
} catch (error) {
if (formatBlockfrostError(error).status_code === 404) {
return 0n;
}
throw error;
}
};

const fetchRedeemers = async ({
redeemer_count,
hash
Expand Down Expand Up @@ -329,35 +315,6 @@ export const blockfrostWalletProvider = (blockfrost: BlockFrostAPI, logger = dum
return transactionsByHashes(addressTransactionsSinceBlock.map(({ tx_hash }) => Cardano.TransactionId(tx_hash)));
};

const accountRewards = async (
stakeAddress: Cardano.RewardAccount,
{ lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER }: EpochRange = {}
): Promise<EpochRewards[]> => {
const result: EpochRewards[] = [];
const batchSize = 100;
let page = 1;
let haveMorePages = true;
while (haveMorePages) {
const rewardsPage = await blockfrost.accountsRewards(stakeAddress.toString(), { count: batchSize, page });
result.push(
...rewardsPage
.filter(({ epoch }) => lowerBound <= epoch && epoch <= upperBound)
.map(({ epoch, amount }) => ({
epoch,
rewards: BigInt(amount)
}))
);
haveMorePages = rewardsPage.length === batchSize && rewardsPage[rewardsPage.length - 1].epoch < upperBound;
page += 1;
}
return result;
};

const rewardsHistory: WalletProvider['rewardsHistory'] = async ({ rewardAccounts, epochs }) => {
const allAddressRewards = await Promise.all(rewardAccounts.map((address) => accountRewards(address, epochs)));
return new Map(allAddressRewards.map((epochRewards, i) => [rewardAccounts[i], epochRewards]));
};

const genesisParameters: WalletProvider['genesisParameters'] = async () => {
const response = await blockfrost.genesis();
return {
Expand Down Expand Up @@ -407,8 +364,6 @@ export const blockfrostWalletProvider = (blockfrost: BlockFrostAPI, logger = dum
currentWalletProtocolParameters,
genesisParameters,
ledgerTip,
rewardAccountBalance: rewards,
rewardsHistory,
stakePoolStats,
transactionsByAddresses,
transactionsByHashes,
Expand Down
1 change: 1 addition & 0 deletions packages/blockfrost/src/index.ts
Expand Up @@ -3,5 +3,6 @@ export * from './blockfrostWalletProvider';
export * from './blockfrostAssetProvider';
export * from './blockfrostTxSubmitProvider';
export * from './blockfrostNetworkInfoProvider';
export * from './blockfrostRewardsProvider';
export { Options } from '@blockfrost/blockfrost-js/lib/types';
export { BlockFrostAPI } from '@blockfrost/blockfrost-js';
139 changes: 139 additions & 0 deletions packages/blockfrost/test/blockfrostRewardsProvider.test.ts
@@ -0,0 +1,139 @@
import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
import { Cardano, ProviderError, ProviderFailure, RewardsProvider } from '@cardano-sdk/core';
import { blockfrostRewardsProvider } from '../src';
jest.mock('@blockfrost/blockfrost-js');

describe('blockfrostRewardsProvider', () => {
const apiKey = 'someapikey';

describe('healthCheck', () => {
it('returns ok if the service reports a healthy state', async () => {
BlockFrostAPI.prototype.health = jest.fn().mockResolvedValue({ is_healthy: true });
const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
const provider = blockfrostRewardsProvider(blockfrost);
expect(await provider.healthCheck()).toEqual({ ok: true });
});
it('returns not ok if the service reports an unhealthy state', async () => {
BlockFrostAPI.prototype.health = jest.fn().mockResolvedValue({ is_healthy: false });
const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
const provider = blockfrostRewardsProvider(blockfrost);
expect(await provider.healthCheck()).toEqual({ ok: false });
});
it('throws a typed error if caught during the service interaction', async () => {
BlockFrostAPI.prototype.health = jest
.fn()
.mockRejectedValue(new ProviderError(ProviderFailure.Unknown, new Error('Some error')));
const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
const provider = blockfrostRewardsProvider(blockfrost);
await expect(provider.healthCheck()).rejects.toThrowError(ProviderError);
});
});
describe('rewardAccountBalance', () => {
test('used reward account', async () => {
const accountsMockResponse = {
active: true,
active_epoch: 81,
controlled_amount: '95565690389731',
pool_id: 'pool1y6chk7x7fup4ms9leesdr57r4qy9cwxuee0msan72x976a6u0nc',
reserves_sum: '0',
rewards_sum: '615803862289',
stake_address: 'stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27',
treasury_sum: '0',
withdrawable_amount: '615803862289',
withdrawals_sum: '0'
};
BlockFrostAPI.prototype.accounts = jest.fn().mockResolvedValue(accountsMockResponse);

const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
const client = blockfrostRewardsProvider(blockfrost);
const response = await client.rewardAccountBalance(
Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27')
);

expect(response).toEqual(BigInt(accountsMockResponse.withdrawable_amount));
});

test('unused reward account', async () => {
const notFoundBody = {
error: 'Not Found',
message: 'The requested component has not been found.',
status_code: 404
};
BlockFrostAPI.prototype.accounts = jest.fn().mockRejectedValue(notFoundBody);

const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
const client = blockfrostRewardsProvider(blockfrost);
const response = await client.rewardAccountBalance(
Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d')
);
expect(response).toEqual(0n);
});
});

describe('rewardsHistory', () => {
const pool_id = 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy';
const rewardAccounts = [
'stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27',
'stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d'
].map(Cardano.RewardAccount);
const generateRewardsResponse = (numEpochs: number, firstEpoch = 0): Responses['account_reward_content'] =>
[...Array.from({ length: numEpochs }).keys()].map((epoch) => ({
amount: '1000',
epoch: firstEpoch + epoch,
pool_id
}));
let client: RewardsProvider;

beforeEach(() => {
const blockfrost = new BlockFrostAPI({ isTestnet: true, projectId: apiKey });
client = blockfrostRewardsProvider(blockfrost);
});

test('epoch bounds & query per stake address', async () => {
BlockFrostAPI.prototype.accountsRewards = jest.fn().mockResolvedValue(generateRewardsResponse(2, 98));

const response = await client.rewardsHistory({
epochs: {
lowerBound: 98,
upperBound: 98
},
rewardAccounts
});

expect(BlockFrostAPI.prototype.accountsRewards).toBeCalledTimes(2);
expect(response).toEqual(
new Map([
[rewardAccounts[0], [{ epoch: 98, rewards: 1000n }]],
[rewardAccounts[1], [{ epoch: 98, rewards: 1000n }]]
])
);
});

test('pagination', async () => {
BlockFrostAPI.prototype.accountsRewards = jest
.fn()
.mockResolvedValueOnce(generateRewardsResponse(100))
.mockResolvedValueOnce(generateRewardsResponse(0));

const response = await client.rewardsHistory({
epochs: {
lowerBound: 98
},
rewardAccounts: [rewardAccounts[0]]
});

expect(BlockFrostAPI.prototype.accountsRewards).toBeCalledTimes(2);
expect(response).toEqual(
new Map([
[
rewardAccounts[0],
[
{ epoch: 98, rewards: 1000n },
{ epoch: 99, rewards: 1000n }
]
]
])
);
});
});
});

0 comments on commit 3a0a2f4

Please sign in to comment.