From dd12ee6c704f016210711d77d8f0d9646510ac79 Mon Sep 17 00:00:00 2001 From: Rhys Bartels-Waller Date: Tue, 19 Nov 2024 13:00:52 +0000 Subject: [PATCH] feat: browser compatible BlockfrostRewardsProvider --- .../BlockfrostRewardsProvider.ts | 20 +- .../src/RewardsProvider/index.ts | 1 + .../BlockfrostRewardsProvider.test.ts | 106 +++++++++++ .../src/Program/programs/providerServer.ts | 5 +- .../BlockfrostRewardsProvider/index.ts | 1 - .../cardano-services/src/Rewards/index.ts | 1 - .../BlockfrostRewardsProvider.test.ts | 175 ------------------ 7 files changed, 126 insertions(+), 183 deletions(-) rename packages/{cardano-services/src/Rewards/BlockfrostRewardsProvider => cardano-services-client/src/RewardsProvider}/BlockfrostRewardsProvider.ts (75%) create mode 100644 packages/cardano-services-client/test/RewardProvider/BlockfrostRewardsProvider.test.ts delete mode 100644 packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/index.ts delete mode 100644 packages/cardano-services/test/Reward/BlockfrostRewardProvider/BlockfrostRewardsProvider.test.ts diff --git a/packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/BlockfrostRewardsProvider.ts b/packages/cardano-services-client/src/RewardsProvider/BlockfrostRewardsProvider.ts similarity index 75% rename from packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/BlockfrostRewardsProvider.ts rename to packages/cardano-services-client/src/RewardsProvider/BlockfrostRewardsProvider.ts index db3fdd4b5f4..c85e350e645 100644 --- a/packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/BlockfrostRewardsProvider.ts +++ b/packages/cardano-services-client/src/RewardsProvider/BlockfrostRewardsProvider.ts @@ -1,15 +1,25 @@ import { Cardano, Reward, RewardAccountBalanceArgs, RewardsHistoryArgs, RewardsProvider } from '@cardano-sdk/core'; -import { BlockfrostProvider } from '../../util/BlockfrostProvider/BlockfrostProvider'; +import { + BlockfrostClient, + BlockfrostProvider, + blockfrostToProviderError, + isBlockfrostNotFoundError +} from '../blockfrost'; +import { Logger } from 'ts-log'; import { Range } from '@cardano-sdk/util'; -import { blockfrostToProviderError, isBlockfrostNotFoundError } from '@cardano-sdk/cardano-services-client'; +import { Responses } from '@blockfrost/blockfrost-js'; const stringToBigInt = (str: string) => BigInt(str); export class BlockfrostRewardsProvider extends BlockfrostProvider implements RewardsProvider { + constructor(client: BlockfrostClient, logger: Logger) { + super(client, logger); + } + public async rewardAccountBalance({ rewardAccount }: RewardAccountBalanceArgs) { try { - const accountResponse = await this.blockfrost.accounts(rewardAccount.toString()); + const accountResponse = await this.request(`accounts/${rewardAccount.toString()}`); return BigInt(accountResponse.withdrawable_amount); } catch (error) { if (isBlockfrostNotFoundError(error)) { @@ -31,7 +41,9 @@ export class BlockfrostRewardsProvider extends BlockfrostProvider implements Rew let page = 1; let haveMorePages = true; while (haveMorePages) { - const rewardsPage = await this.blockfrost.accountsRewards(stakeAddress.toString(), { count: batchSize, page }); + const rewardsPage = await this.request( + `accounts/${stakeAddress.toString()}/rewards?count=${batchSize}?page=${page}` + ); result.push( ...rewardsPage diff --git a/packages/cardano-services-client/src/RewardsProvider/index.ts b/packages/cardano-services-client/src/RewardsProvider/index.ts index 4aad9dd93ff..a96739baa31 100644 --- a/packages/cardano-services-client/src/RewardsProvider/index.ts +++ b/packages/cardano-services-client/src/RewardsProvider/index.ts @@ -1 +1,2 @@ +export * from './BlockfrostRewardsProvider'; export * from './rewardsHttpProvider'; diff --git a/packages/cardano-services-client/test/RewardProvider/BlockfrostRewardsProvider.test.ts b/packages/cardano-services-client/test/RewardProvider/BlockfrostRewardsProvider.test.ts new file mode 100644 index 00000000000..80a6ac6b990 --- /dev/null +++ b/packages/cardano-services-client/test/RewardProvider/BlockfrostRewardsProvider.test.ts @@ -0,0 +1,106 @@ +import { BlockfrostClient, BlockfrostRewardsProvider } from '../../src'; +import { Cardano } from '@cardano-sdk/core'; +import { Responses } from '@blockfrost/blockfrost-js'; +import { logger } from '@cardano-sdk/util-dev'; +import { mockResponses } from '../AssetInfoProvider/util'; +jest.mock('@blockfrost/blockfrost-js'); + +describe('blockfrostRewardsProvider', () => { + let request: jest.Mock; + let provider: BlockfrostRewardsProvider; + + beforeEach(async () => { + request = jest.fn(); + const client = { request } as unknown as BlockfrostClient; + provider = new BlockfrostRewardsProvider(client, logger); + }); + + describe('rewardAccountBalance', () => { + test('used reward account', async () => { + const mockedAccountsResponse = { + 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' + }; + mockResponses(request, [ + ['accounts/stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27', mockedAccountsResponse] + ]); + + const response = await provider.rewardAccountBalance({ + rewardAccount: Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27') + }); + + expect(response).toEqual(BigInt(mockedAccountsResponse.withdrawable_amount)); + }); + }); + + 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, + type: 'member' + })); + + test('epoch bounds & query per stake address', async () => { + mockResponses(request, [ + [`accounts/${rewardAccounts[0]}/rewards?count=100?page=1`, generateRewardsResponse(2, 98)], + [`accounts/${rewardAccounts[1]}/rewards?count=100?page=1`, generateRewardsResponse(2, 98)] + ]); + + const response = await provider.rewardsHistory({ + epochs: { + lowerBound: Cardano.EpochNo(98), + upperBound: Cardano.EpochNo(98) + }, + rewardAccounts + }); + + expect(response).toEqual( + new Map([ + [rewardAccounts[0], [{ epoch: 98, rewards: 1000n }]], + [rewardAccounts[1], [{ epoch: 98, rewards: 1000n }]] + ]) + ); + }); + + test('pagination', async () => { + mockResponses(request, [ + [`accounts/${rewardAccounts[0]}/rewards?count=100?page=1`, generateRewardsResponse(100)], + [`accounts/${rewardAccounts[0]}/rewards?count=100?page=2`, generateRewardsResponse(0)] + ]); + + const response = await provider.rewardsHistory({ + epochs: { + lowerBound: Cardano.EpochNo(98) + }, + rewardAccounts: [rewardAccounts[0]] + }); + + expect(response).toEqual( + new Map([ + [ + rewardAccounts[0], + [ + { epoch: 98, rewards: 1000n }, + { epoch: 99, rewards: 1000n } + ] + ] + ]) + ); + }); + }); +}); diff --git a/packages/cardano-services/src/Program/programs/providerServer.ts b/packages/cardano-services/src/Program/programs/providerServer.ts index 4f94da09f1f..a7a43b7fa89 100644 --- a/packages/cardano-services/src/Program/programs/providerServer.ts +++ b/packages/cardano-services/src/Program/programs/providerServer.ts @@ -18,6 +18,7 @@ import { BlockfrostAssetProvider, BlockfrostChainHistoryProvider, BlockfrostNetworkInfoProvider, + BlockfrostRewardsProvider, BlockfrostUtxoProvider, CardanoWsClient, TxSubmitApiProvider @@ -40,7 +41,6 @@ import { StubTokenMetadataService, TypeormAssetProvider } from '../../Asset'; -import { BlockfrostRewardsProvider, DbSyncRewardsProvider, RewardsHttpService } from '../../Rewards'; import { BlockfrostTxSubmitProvider, NodeTxSubmitProvider, TxSubmitHttpService } from '../../TxSubmit'; import { ChainHistoryHttpService, DbSyncChainHistoryProvider } from '../../ChainHistory'; import { @@ -54,6 +54,7 @@ import { } from '../options'; import { DbPools, DbSyncEpochPollService, TypeormProvider, getBlockfrostApi, getBlockfrostClient } from '../../util'; import { DbSyncNetworkInfoProvider, NetworkInfoHttpService } from '../../NetworkInfo'; +import { DbSyncRewardsProvider, RewardsHttpService } from '../../Rewards'; import { DbSyncStakePoolProvider, StakePoolHttpService, @@ -344,7 +345,7 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => { const getBlockfrostChainHistoryProvider = (nInfoProvider: NetworkInfoProvider | DbSyncNetworkInfoProvider) => new BlockfrostChainHistoryProvider(getBlockfrostClient(), nInfoProvider, logger); - const getBlockfrostRewardsProvider = () => new BlockfrostRewardsProvider({ blockfrost: getBlockfrostApi(), logger }); + const getBlockfrostRewardsProvider = () => new BlockfrostRewardsProvider(getBlockfrostClient(), logger); const getDbSyncRewardsProvider = withDbSyncProvider( (dbPools, cardanoNode) => diff --git a/packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/index.ts b/packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/index.ts deleted file mode 100644 index 3703ef15d9a..00000000000 --- a/packages/cardano-services/src/Rewards/BlockfrostRewardsProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BlockfrostRewardsProvider'; diff --git a/packages/cardano-services/src/Rewards/index.ts b/packages/cardano-services/src/Rewards/index.ts index 8a476915844..f902bb2cd24 100644 --- a/packages/cardano-services/src/Rewards/index.ts +++ b/packages/cardano-services/src/Rewards/index.ts @@ -1,3 +1,2 @@ -export * from './BlockfrostRewardsProvider'; export * from './DbSyncRewardProvider'; export * from './RewardsHttpService'; diff --git a/packages/cardano-services/test/Reward/BlockfrostRewardProvider/BlockfrostRewardsProvider.test.ts b/packages/cardano-services/test/Reward/BlockfrostRewardProvider/BlockfrostRewardsProvider.test.ts deleted file mode 100644 index 5ca041ab1b3..00000000000 --- a/packages/cardano-services/test/Reward/BlockfrostRewardProvider/BlockfrostRewardsProvider.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js'; -import { BlockfrostRewardsProvider } from '../../../src'; -import { Cardano, ProviderError, ProviderFailure } from '@cardano-sdk/core'; -import { logger } from '@cardano-sdk/util-dev'; -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({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - 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({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - 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({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - 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({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - const response = await provider.rewardAccountBalance({ - rewardAccount: Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27') - }); - - expect(response).toEqual(BigInt(accountsMockResponse.withdrawable_amount)); - }); - - test('unused reward account', async () => { - BlockFrostAPI.prototype.accounts = jest.fn().mockRejectedValue({ - error: 'Not Found', - message: 'The requested component has not been found.', - status_code: 404, - url: 'some-url' - }); - const blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - const response = await provider.rewardAccountBalance({ - rewardAccount: 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, - type: 'member' - })); - let provider: BlockfrostRewardsProvider; - - beforeEach(() => { - const blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - provider = new BlockfrostRewardsProvider({ blockfrost, logger }); - }); - - test('epoch bounds & query per stake address', async () => { - BlockFrostAPI.prototype.accountsRewards = jest.fn().mockResolvedValue(generateRewardsResponse(2, 98)); - - const response = await provider.rewardsHistory({ - epochs: { - lowerBound: Cardano.EpochNo(98), - upperBound: Cardano.EpochNo(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 provider.rewardsHistory({ - epochs: { - lowerBound: Cardano.EpochNo(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 } - ] - ] - ]) - ); - }); - - const mockedError = { - error: 'Forbidden', - message: 'Invalid project token.', - status_code: 403, - url: 'test' - }; - - const mockedErrorMethod = jest.fn().mockRejectedValue(mockedError); - - test('rewardsHistory throws', async () => { - BlockFrostAPI.prototype.accountsRewards = mockedErrorMethod; - - await expect(() => - provider.rewardsHistory({ - epochs: { - lowerBound: Cardano.EpochNo(98) - }, - rewardAccounts: [rewardAccounts[0]] - }) - ).rejects.toThrow(); - expect(mockedErrorMethod).toBeCalledTimes(1); - }); - - test('rewardAccountBalance throws', async () => { - mockedErrorMethod.mockClear(); - BlockFrostAPI.prototype.accounts = mockedErrorMethod; - - await expect(() => - provider.rewardAccountBalance({ - rewardAccount: Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d') - }) - ).rejects.toThrow(); - expect(mockedErrorMethod).toBeCalledTimes(1); - }); - }); -});