diff --git a/data/network-fees.js b/data/network-fees.js index 38254617c2..46c5e382b5 100644 --- a/data/network-fees.js +++ b/data/network-fees.js @@ -14,7 +14,8 @@ const getNetworkTransactionGasEstimates = (networkId, transactionType) => { } const terraGasEstimates = { - default: 300000 + default: 300000, + ClaimRewardsTx: 550000 } const cosmosGasEstimates = { diff --git a/lib/reducers/cosmosV0-reducers.js b/lib/reducers/cosmosV0-reducers.js index 93fd9d025f..6b977c4bd6 100644 --- a/lib/reducers/cosmosV0-reducers.js +++ b/lib/reducers/cosmosV0-reducers.js @@ -1,4 +1,3 @@ -const { flatten } = require('lodash') const BigNumber = require('bignumber.js') /** @@ -375,22 +374,12 @@ async function overviewReducer( balances, delegations, undelegations, - rewards, stakingDenom, fiatValueAPI, fiatCurrency, reducers ) { stakingDenom = denomLookup(stakingDenom) - - const totalRewards = flatten(rewards) - // this filter is here for multidenom networks. If there is the field denoms inside rewards, we filter - // only the staking denom rewards for 'totalRewards' - .filter(reward => - reward.denom ? denomLookup(reward.denom) === stakingDenom : reward - ) - .reduce((sum, { amount }) => BigNumber(sum).plus(amount), 0) - .toFixed(6) const liquidStake = BigNumber( ( balances.find(({ denom }) => denomLookup(denom) === stakingDenom) || { @@ -409,14 +398,6 @@ async function overviewReducer( const totalStake = liquidStake.plus(delegatedStake).plus(undelegatingStake) return { - rewards: - // there are some accounts which are not staking and therefore have no rewards - rewards[0] - ? rewards[0].constructor === Array - ? reducers.rewardReducer(rewards) - : rewards - : null, - totalRewards: totalRewards, liquidStake: liquidStake, totalStake, totalStakeFiatValue: fiatValueAPI diff --git a/lib/resolvers.js b/lib/resolvers.js index c01af5a4eb..024a0b67bd 100644 --- a/lib/resolvers.js +++ b/lib/resolvers.js @@ -13,6 +13,7 @@ const { getNetworkTransactionGasEstimates } = require('../data/network-fees') const database = require('./database') const config = require('../config.js') const { logOverview } = require('./statistics') +const BigNumber = require('bignumber.js') function createDBInstance(network) { const networkSchemaName = network ? network.replace(/-/g, '_') : false @@ -181,7 +182,41 @@ const resolvers = { accountInformation: (account, _, { dataSources }) => remoteFetch(dataSources, account.networkId).getAccountInfo( account.address + ), + rewards: async ( + { networkId, address, fiatCurrency }, + _, + { dataSources } + ) => { + await localStore(dataSources, networkId).dataReady + const validatorsDictionary = localStore(dataSources, networkId).validators + return remoteFetch(dataSources, networkId).getRewards( + address, + validatorsDictionary, + fiatCurrency + ) + }, + totalRewards: async ( + { networkId, address, fiatCurrency }, + _, + { dataSources } + ) => { + await localStore(dataSources, networkId).dataReady + const validatorsDictionary = localStore(dataSources, networkId).validators + const rewards = await remoteFetch(dataSources, networkId).getRewards( + address, + validatorsDictionary, + fiatCurrency ) + const stakingDenom = await remoteFetch( + dataSources, + networkId + ).getStakingViewDenom() + return rewards + .filter(({ denom }) => denom === stakingDenom) + .reduce((sum, { amount }) => BigNumber(sum).plus(amount), 0) + .toFixed(6) + } }, Proposal: { validator: (proposal, _, { dataSources }) => { diff --git a/lib/source/cosmosV0-source.js b/lib/source/cosmosV0-source.js index 8102aa36d6..53f79fed3c 100644 --- a/lib/source/cosmosV0-source.js +++ b/lib/source/cosmosV0-source.js @@ -27,6 +27,7 @@ class CosmosV0API extends RESTDataSource { this.validatorConsensusBech32Prefix = `${network.address_prefix}valcons` this.gasPrices = gasPrices this.store = store + this.viewDenom = network.coinLookup[0].viewDenom this.setReducers() } @@ -56,7 +57,7 @@ class CosmosV0API extends RESTDataSource { if ((await this.cache.getTotalSize()) > 100000) { await this.cache.flush() } - // cleareing memoizedResults + // clearing memoizedResults this.memoizedResults.clear() try { return await this.get(url, null, { cacheOptions: { ttl: 1 } }) // normally setting cacheOptions should be enought, but... @@ -87,6 +88,10 @@ class CosmosV0API extends RESTDataSource { return stakingParameters.bond_denom } + getStakingViewDenom() { + return this.viewDenom + } + async getSignedBlockWindow() { const slashingParams = await this.query('/slashing/parameters') return slashingParams.signed_blocks_window @@ -493,17 +498,11 @@ class CosmosV0API extends RESTDataSource { ), this.getStakingDenom() ]) - const rewards = await this.getRewards( - delegatorAddress, - validatorsDictionary, - fiatCurrency - ) const fiatValueAPI = this.calculateFiatValue ? this : null return this.reducers.overviewReducer( balances, delegations, undelegations, - rewards, stakingDenom, fiatValueAPI, fiatCurrency, diff --git a/lib/source/cosmosV2-source.js b/lib/source/cosmosV2-source.js index 714429e6d0..2757dd6414 100644 --- a/lib/source/cosmosV2-source.js +++ b/lib/source/cosmosV2-source.js @@ -156,6 +156,7 @@ class CosmosV2API extends CosmosV0API { const result = await this.query( `distribution/delegators/${delegatorAddress}/rewards` ) + return (result.rewards || []) .filter(({ reward }) => reward && reward.length > 0) .map(({ reward, validator_address }) => diff --git a/lib/source/polkadotV0-source.js b/lib/source/polkadotV0-source.js index 0215056a66..cf2f6eec71 100644 --- a/lib/source/polkadotV0-source.js +++ b/lib/source/polkadotV0-source.js @@ -3,6 +3,7 @@ const BigNumber = require('bignumber.js') class polkadotAPI { constructor(network, store) { this.network = network + this.stakingViewDenom = network.coinLookup[0].viewDenom this.setReducers() this.store = store } @@ -49,7 +50,7 @@ class polkadotAPI { ) return this.reducers.blockReducer( - this.networkId, + this.network.id, blockHeight, blockHash, sessionIndex.toNumber(), @@ -171,8 +172,6 @@ class polkadotAPI { totalStake: accountBalances[0].total, totalStakeFiatValue: '', liquidStake: accountBalances[0].amount, - totalRewards: '', - rewards: undefined, accountInformation: undefined } } @@ -202,6 +201,10 @@ class polkadotAPI { getUndelegationsForDelegatorAddress() { return [] } + + getStakingViewDenom() { + return this.stakingViewDenom + } } module.exports = polkadotAPI diff --git a/lib/statistics.js b/lib/statistics.js index a980f3c86f..11aea3834f 100644 --- a/lib/statistics.js +++ b/lib/statistics.js @@ -123,9 +123,11 @@ const logOverview = (overview, fingerprint) => { data.value = overview.totalStake.toString() store(data) // store totalRewards - data.action = 'totalRewards' - data.value = overview.totalRewards.toString() - store(data) + if (overview.totalRewards) { + data.action = 'totalRewards' + data.value = overview.totalRewards.toString() + store(data) + } // store rewards // summing rewards with one denom if (overview.rewards) { diff --git a/tests/source/cosmosv0-source.test.js b/tests/source/cosmosv0-source.test.js new file mode 100644 index 0000000000..38c8b9b286 --- /dev/null +++ b/tests/source/cosmosv0-source.test.js @@ -0,0 +1,25 @@ +const CosmosV0API = require('../../lib/source/cosmosV0-source') +const networks = require('../../data/networks') + +describe('Cosmos V0 API', function() { + describe('checkAddress()', function() { + let api, cosmosNetworkConfig + + beforeEach(() => { + cosmosNetworkConfig = networks.find( + network => network.id === 'cosmos-hub-testnet' + ) + api = new CosmosV0API(cosmosNetworkConfig, {}) + }) + + it('When a valid prefix is passed, it should not throw an error', () => { + expect(() => + api.checkAddress(`${cosmosNetworkConfig.bech32_prefix}12345ABCDE`) + ).not.toThrow() + }) + + it('When an invalid prefix is passed, it should throw an error', () => { + expect(() => api.checkAddress('12345ABCDE')).toThrow() + }) + }) +}) diff --git a/tests/source/cosmosv2-source.test.js b/tests/source/cosmosv2-source.test.js new file mode 100644 index 0000000000..7b6ccb2c09 --- /dev/null +++ b/tests/source/cosmosv2-source.test.js @@ -0,0 +1,98 @@ +const CosmosV2API = require('../../lib/source/cosmosV2-source') +const networks = require('../../data/networks') +const { + delegatorAddress, + mockValidatorsDictionary, + delegatorRewards +} = require('./mock_data/delegators') + +let mockDelegatorRewards = { ...delegatorRewards } +jest.mock('apollo-datasource-rest', () => { + class MockRESTDataSource { + constructor() { + this.memoizedResults = { + clear: jest.fn() + } + } + + get() { + return mockDelegatorRewards + } + } + + return { + RESTDataSource: MockRESTDataSource, + HTTPCache: class MockHTTPCache {} + } +}) + +describe('Cosmos V2 API', function() { + describe('getRewards()', function() { + let api, cosmosNetworkConfig + + beforeEach(() => { + cosmosNetworkConfig = networks.find( + network => network.id === 'cosmos-hub-testnet' + ) + api = new CosmosV2API(cosmosNetworkConfig, {}) + mockDelegatorRewards = JSON.parse(JSON.stringify(delegatorRewards)) + }) + + it('When an existing delegator address is passed, it should return the calculated rewards', async () => { + //Act + const result = await api.getRewards( + delegatorAddress, + mockValidatorsDictionary + ) + + //Assert + expect(result[0]).toHaveProperty('amount') + expect(result[0]).toHaveProperty('denom') + expect(result[0].validator).toEqual( + mockValidatorsDictionary[delegatorAddress] + ) + }) + + it('When an existing delegator address has no rewards, it should return no rewards', async () => { + //Arrange + mockDelegatorRewards.result.rewards = [] + + //Act & Assert + await expect( + api.getRewards(delegatorAddress, mockValidatorsDictionary) + ).resolves.toEqual([]) + }) + + it('When an existing delegator address is passed with a reward 49000000 (umuon), it should return amount 49 (muon)', async () => { + //Arrange + mockDelegatorRewards.result.rewards[0].reward[0].amount = 49000000 + + //Act & Assert + await expect( + api.getRewards(delegatorAddress, mockValidatorsDictionary) + ).resolves.toEqual([ + { + amount: '49.000000', + denom: 'MUON', + validator: mockValidatorsDictionary[delegatorAddress] + } + ]) + }) + + it('When an existing delegator address is passed with a reward < 1 (umuon), it should return amount = 0 (muon)', async () => { + //Arrange + mockDelegatorRewards.result.rewards[0].reward[0].amount = 0.05 + + //Act & Assert + await expect( + api.getRewards(delegatorAddress, mockValidatorsDictionary) + ).resolves.toEqual([ + { + amount: '0.000000', + denom: 'MUON', + validator: mockValidatorsDictionary[delegatorAddress] + } + ]) + }) + }) +}) diff --git a/tests/source/mock_data/delegators.js b/tests/source/mock_data/delegators.js new file mode 100644 index 0000000000..817d3fd9f3 --- /dev/null +++ b/tests/source/mock_data/delegators.js @@ -0,0 +1,54 @@ +const delegatorAddress = 'cosmosvaloper1y3v65vhz5f93k8uk7vnz5v7yr7ks5gdcumgcqr' + +const mockValidatorsDictionary = { + [delegatorAddress]: { + networkId: 'cosmos-hub-testnet', + operatorAddress: delegatorAddress, + consensusPubkey: + 'cosmosvalconspub1zcjduepqndtvyf7ggaamy2qzafjm9fnn3us4fllv89wtl40nt586q5g90rnsaxtvlz', + jailed: false, + details: '', + website: 'https://fissionlabs.io', + identity: '', + name: 'Fission Labs', + votingPower: '0.084015352', + startHeight: '0', + uptimePercentage: 1, + tokens: '60002.000010', + commissionUpdateTime: '2019-12-20T17:00:00Z', + commission: '0.500000000000000000', + maxCommission: '1.000000000000000000', + maxChangeCommission: '1.000000000000000000', + status: 'ACTIVE', + statusDetailed: 'active', + delegatorShares: '60002000010.000000000000000000' + } +} + +const delegatorRewards = { + result: { + rewards: [ + { + validator_address: delegatorAddress, + reward: [ + { + denom: 'umuon', + amount: '0.107080042987452090' + } + ] + } + ], + total: [ + { + denom: 'umuon', + amount: '0.107080042987452090' + } + ] + } +} + +module.exports = { + delegatorAddress, + mockValidatorsDictionary, + delegatorRewards +}