Skip to content

Commit

Permalink
Merge branch 'development' into SERVICES-1760-airbnb-eslint
Browse files Browse the repository at this point in the history
  • Loading branch information
claudiulataretu committed Aug 22, 2023
2 parents 058985e + 32e81ac commit 26d8331
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit.tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion src/modules/governance/event-decoder/governance.event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/modules/governance/governance.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -45,6 +45,7 @@ import { GovernanceProposalResolver } from './resolvers/governance.proposal.reso
GovernanceEnergyContractResolver,
GovernanceTokenSnapshotContractResolver,
GovernanceProposalResolver,
ElasticService,
],
exports: [
GovernanceTokenSnapshotAbiService,
Expand Down
90 changes: 68 additions & 22 deletions src/modules/governance/services/governance.compute.service.ts
Original file line number Diff line number Diff line change
@@ -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<VoteType> {
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<any[]> {
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;
}
}
15 changes: 4 additions & 11 deletions src/modules/governance/services/governance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,12 @@ export class GovernanceService {
}

async userVote(contractAddress: string, proposalId: number, userAddress?: string): Promise<VoteType> {
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<EsdtToken> {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/governance/services/governance.setter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export class GovernanceSetterService extends GenericSetterService {
);
}

async userVotedProposalsWithVoteType(scAddress: string, userAddress: string, value: { proposalId: number, vote: VoteType }[]): Promise<string> {
async userVoteTypesForContract(scAddress: string, userAddress: string, value: { proposalId: number, vote: VoteType }[]): Promise<string> {
return await this.setData(
this.getCacheKey('userVotedProposalsWithVoteType', scAddress, userAddress),
this.getCacheKey('userVoteTypesForContract', scAddress, userAddress),
value,
CacheTtlInfo.ContractState.remoteTtl,
CacheTtlInfo.ContractState.localTtl,
Expand Down
22 changes: 12 additions & 10 deletions src/modules/rabbitmq/handlers/governance.handler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
10 changes: 10 additions & 0 deletions src/modules/staking/services/staking.compute.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions src/modules/staking/specs/staking.compute.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,27 @@ describe('StakingComputeService', () => {
}),
);
});

it('should NOT compute optimal compound frequency', async () => {
const service = module.get<StakingComputeService>(
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,
}),
);
});
});

0 comments on commit 26d8331

Please sign in to comment.