diff --git a/.github/workflows/unit.tests.yml b/.github/workflows/unit.tests.yml index 629dc977e..204a498c9 100644 --- a/.github/workflows/unit.tests.yml +++ b/.github/workflows/unit.tests.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ redis-version: [6] diff --git a/Dockerfile b/Dockerfile index e0adf37c7..5f7ffb1c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.22.0-alpine3.12 as build +FROM node:18.17.1-alpine3.18 as build WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH @@ -16,7 +16,7 @@ RUN npm ci COPY . ./ RUN npm run build -FROM node:12.22.0-alpine3.12 +FROM node:18.17.1-alpine3.18 WORKDIR /app COPY --from=build /app /app diff --git a/README.md b/README.md index 25f569bfb..1fb6714f8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ GraphQl service to provide backend environment for xExchange ## Dependencies -1. Node.js > @16.x.x is required to be installed [docs](https://nodejs.org/en/) +1. Node.js > @18.x.x is required to be installed [docs](https://nodejs.org/en/) 2. Redis Server is required to be installed [docs](https://redis.io/). 3. RabbitMQ Server is required to be installed [docs](https://www.rabbitmq.com/download.html). 4. MongoDB Server is required to be installed [docs](https://www.mongodb.com/docs/manual/installation). diff --git a/src/modules/governance/event-decoder/governance.event.ts b/src/modules/governance/event-decoder/governance.event.ts index 861ad141a..c3c7ba51c 100644 --- a/src/modules/governance/event-decoder/governance.event.ts +++ b/src/modules/governance/event-decoder/governance.event.ts @@ -9,7 +9,7 @@ export enum GOVERNANCE_EVENTS { ABSTAIN = "abstainVoteCast", } -export function convertToVoteType(event: GOVERNANCE_EVENTS): VoteType { +export function convertToVoteType(event: GOVERNANCE_EVENTS | string): VoteType { switch(event) { case GOVERNANCE_EVENTS.UP: return VoteType.UpVote; diff --git a/src/modules/governance/governance.module.ts b/src/modules/governance/governance.module.ts index 43c99c905..e167612d5 100644 --- a/src/modules/governance/governance.module.ts +++ b/src/modules/governance/governance.module.ts @@ -19,7 +19,7 @@ import { import { GovernanceSetterService } from './services/governance.setter.service'; import { GovernanceQueryResolver } from './resolvers/governance.query.resolver'; import { GovernanceProposalResolver } from './resolvers/governance.proposal.resolver'; - +import { ElasticService } from 'src/helpers/elastic.service'; @Module({ imports: [ @@ -45,6 +45,7 @@ import { GovernanceProposalResolver } from './resolvers/governance.proposal.reso GovernanceEnergyContractResolver, GovernanceTokenSnapshotContractResolver, GovernanceProposalResolver, + ElasticService, ], exports: [ GovernanceTokenSnapshotAbiService, diff --git a/src/modules/governance/services/governance.compute.service.ts b/src/modules/governance/services/governance.compute.service.ts index 02dda70c4..e9e517bf6 100644 --- a/src/modules/governance/services/governance.compute.service.ts +++ b/src/modules/governance/services/governance.compute.service.ts @@ -1,44 +1,90 @@ import { Injectable } from '@nestjs/common'; import { ErrorLoggerAsync } from 'src/helpers/decorators/error.logger'; import { VoteType } from '../models/governance.proposal.model'; -import { MXApiService } from '../../../services/multiversx-communication/mx.api.service'; import { GetOrSetCache } from '../../../helpers/decorators/caching.decorator'; import { CacheTtlInfo } from '../../../services/caching/cache.ttl.info'; +import { ElasticQuery } from '../../../helpers/entities/elastic/elastic.query'; +import { QueryType } from '../../../helpers/entities/elastic/query.type'; +import { ElasticSortOrder } from '../../../helpers/entities/elastic/elastic.sort.order'; +import { ElasticService } from '../../../helpers/elastic.service'; +import { GovernanceSetterService } from './governance.setter.service'; +import { convertToVoteType } from '../event-decoder/governance.event'; +import { Address } from '@multiversx/sdk-core/out'; +import { decimalToHex } from '../../../utils/token.converters'; @Injectable() export class GovernanceComputeService { constructor( - private readonly mxAPI: MXApiService, + private readonly elasticService: ElasticService, + private readonly governanceSetter: GovernanceSetterService, ) { } + async userVotedProposalsWithVoteType(scAddress: string, userAddress: string, proposalId: number): Promise { + const currentCachedProposalVoteTypes = await this.userVoteTypesForContract(scAddress, userAddress); + const cachedVoteType = currentCachedProposalVoteTypes.find((proposal) => proposal.proposalId === proposalId); + if (cachedVoteType) { + return cachedVoteType.vote; + } + + const log = await this.getVoteLog('vote', scAddress, userAddress, proposalId); + let voteType = VoteType.NotVoted; + if (log.length > 0) { + const voteEvent = log[0]._source.events.find((event) => event.identifier === 'vote'); + voteType = convertToVoteType(atob(voteEvent.topics[0])); + } + const proposalVoteType = { + proposalId, + vote: voteType, + } + currentCachedProposalVoteTypes.push(proposalVoteType); + await this.governanceSetter.userVoteTypesForContract(scAddress, userAddress, currentCachedProposalVoteTypes); + return proposalVoteType.vote; + } + @ErrorLoggerAsync({ className: GovernanceComputeService.name }) @GetOrSetCache({ baseKey: 'governance', remoteTtl: CacheTtlInfo.ContractState.remoteTtl, localTtl: CacheTtlInfo.ContractState.localTtl, }) - async userVotedProposalsWithVoteType(scAddress: string, userAddress: string): Promise<{ proposalId: number, vote: VoteType }[]> { - return await this.userVotedProposalsWithVoteTypeRaw(scAddress, userAddress); + async userVoteTypesForContract(scAddress: string, userAddress: string): Promise<{ proposalId: number, vote: VoteType }[]> { + return []; } - async userVotedProposalsWithVoteTypeRaw(scAddress: string, userAddress: string): Promise<{ proposalId: number, vote: VoteType }[]> { - const txs = await this.mxAPI.getTransactionsWithOptions({ - sender: userAddress, - receiver: scAddress, - functionName: 'vote', - }) - const proposalWithVoteType = [] - for (const tx of txs) { - if (tx.status !== 'success') { - continue; - } - const data = Buffer.from(tx.data, 'base64').toString('utf-8').split('@'); - proposalWithVoteType.push({ - proposalId: parseInt(data[1], 16), - vote: data[2] === "" ? VoteType.UpVote : parseInt(data[2]), - }); - } - return proposalWithVoteType; + private async getVoteLog( + eventName: string, + scAddress: string, + callerAddress: string, + proposalId: number, + ): Promise { + const elasticQueryAdapter: ElasticQuery = new ElasticQuery(); + const encodedProposalId = Buffer.from(decimalToHex(proposalId), 'hex').toString('base64'); + const encodedCallerAddress = Buffer.from(Address.fromString(callerAddress).hex(), 'hex').toString('base64'); + elasticQueryAdapter.condition.must = [ + QueryType.Match('address', scAddress), + QueryType.Nested('events', [ + QueryType.Match('events.address', scAddress), + QueryType.Match('events.identifier', eventName), + ]), + QueryType.Nested('events', [ + QueryType.Match('events.topics', encodedProposalId), + ]), + QueryType.Nested('events', [ + QueryType.Match('events.topics', encodedCallerAddress), + ]), + ]; + + elasticQueryAdapter.sort = [ + { name: 'timestamp', order: ElasticSortOrder.ascending }, + ]; + + + const list = await this.elasticService.getList( + 'logs', + '', + elasticQueryAdapter, + ); + return list; } } diff --git a/src/modules/governance/services/governance.service.ts b/src/modules/governance/services/governance.service.ts index 8cc7d4355..99080d49b 100644 --- a/src/modules/governance/services/governance.service.ts +++ b/src/modules/governance/services/governance.service.ts @@ -62,19 +62,12 @@ export class GovernanceService { } async userVote(contractAddress: string, proposalId: number, userAddress?: string): Promise { - const userVotesWithType = await this.governanceCompute.userVotedProposalsWithVoteType( - contractAddress, userAddress - ); - - const voteForProposalId = userVotesWithType.find( - value => { - return value.proposalId === proposalId - } - ) - if (!voteForProposalId) { + if (!userAddress) { return VoteType.NotVoted } - return voteForProposalId.vote; + return this.governanceCompute.userVotedProposalsWithVoteType( + contractAddress, userAddress, proposalId + ); } async feeToken(contractAddress: string): Promise { diff --git a/src/modules/governance/services/governance.setter.service.ts b/src/modules/governance/services/governance.setter.service.ts index b24756687..d3c92a417 100644 --- a/src/modules/governance/services/governance.setter.service.ts +++ b/src/modules/governance/services/governance.setter.service.ts @@ -25,9 +25,9 @@ export class GovernanceSetterService extends GenericSetterService { ); } - async userVotedProposalsWithVoteType(scAddress: string, userAddress: string, value: { proposalId: number, vote: VoteType }[]): Promise { + async userVoteTypesForContract(scAddress: string, userAddress: string, value: { proposalId: number, vote: VoteType }[]): Promise { return await this.setData( - this.getCacheKey('userVotedProposalsWithVoteType', scAddress, userAddress), + this.getCacheKey('userVoteTypesForContract', scAddress, userAddress), value, CacheTtlInfo.ContractState.remoteTtl, CacheTtlInfo.ContractState.localTtl, diff --git a/src/modules/rabbitmq/handlers/governance.handler.service.ts b/src/modules/rabbitmq/handlers/governance.handler.service.ts index d71a2323f..1dd07de71 100644 --- a/src/modules/rabbitmq/handlers/governance.handler.service.ts +++ b/src/modules/rabbitmq/handlers/governance.handler.service.ts @@ -37,18 +37,20 @@ export class GovernanceHandlerService { ); this.invalidatedKeys.push(cacheKey); - const userVotedProposalsWithVoteType = await this.governanceCompute.userVotedProposalsWithVoteType(event.address, topics.voter); - userVotedProposalsWithVoteType.push({ - proposalId: topics.proposalId, - vote: convertToVoteType(voteType), - }); - const uniqueUserVotedProposalsWithVoteType = userVotedProposalsWithVoteType.filter((v, i, a) => - a.findIndex(t => (t.proposalId === v.proposalId)) === i - ); - cacheKey = await this.governanceSetter.userVotedProposalsWithVoteType( + const userVotedProposalsWithVoteType = await this.governanceCompute.userVoteTypesForContract(event.address, topics.voter); + const cachedVoteType = userVotedProposalsWithVoteType.find((proposal) => proposal.proposalId === topics.proposalId); + if (!cachedVoteType) { + userVotedProposalsWithVoteType.push({ + proposalId: topics.proposalId, + vote: convertToVoteType(voteType), + }); + } else { + cachedVoteType.vote = convertToVoteType(voteType); + } + cacheKey = await this.governanceSetter.userVoteTypesForContract( event.address, topics.voter, - uniqueUserVotedProposalsWithVoteType, + userVotedProposalsWithVoteType, ); this.invalidatedKeys.push(cacheKey); diff --git a/src/modules/staking/services/staking.compute.service.ts b/src/modules/staking/services/staking.compute.service.ts index 51b8e40e8..debb7716d 100644 --- a/src/modules/staking/services/staking.compute.service.ts +++ b/src/modules/staking/services/staking.compute.service.ts @@ -267,6 +267,16 @@ export class StakingComputeService { } } + if (optimalCompoundIterations === 0) { + return new OptimalCompoundModel({ + optimalProfit: 0, + interval: 0, + days: 0, + hours: 0, + minutes: 0, + }); + } + /* Compute optimal compound frequency expressed in hours and minutes: freqDays = (timeInterval/OptimalCompound) diff --git a/src/modules/staking/specs/staking.compute.service.spec.ts b/src/modules/staking/specs/staking.compute.service.spec.ts index 51d0c9f4c..c698d0826 100644 --- a/src/modules/staking/specs/staking.compute.service.spec.ts +++ b/src/modules/staking/specs/staking.compute.service.spec.ts @@ -161,4 +161,27 @@ describe('StakingComputeService', () => { }), ); }); + + it('should NOT compute optimal compound frequency', async () => { + const service = module.get( + StakingComputeService, + ); + jest.spyOn(service, 'stakeFarmAPR').mockResolvedValue('0.10'); + const optimalCompoundFrequency = + await service.computeOptimalCompoundFrequency( + Address.Zero().bech32(), + '100000000000000000', + 365, + ); + + expect(optimalCompoundFrequency).toEqual( + new OptimalCompoundModel({ + optimalProfit: 0, + interval: 0, + days: 0, + hours: 0, + minutes: 0, + }), + ); + }); });