diff --git a/src/abis/governance-v2.abi.json b/src/abis/governance-v2.abi.json new file mode 100644 index 000000000..8d5bedb8d --- /dev/null +++ b/src/abis/governance-v2.abi.json @@ -0,0 +1,775 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.71.0-nightly", + "commitHash": "77f4f828a2f19854fcbcdf69babe7d0ac1c92852", + "commitDate": "2023-05-20", + "channel": "Nightly", + "short": "rustc 1.71.0-nightly (77f4f828a 2023-05-20)" + }, + "contractCrate": { + "name": "governance-v2", + "version": "0.0.0", + "gitVersion": "v1.6.0-1358-g9b7510d6" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.39.4" + } + }, + "docs": [ + "An empty contract. To be used as a template when starting a new contract from scratch." + ], + "name": "GovernanceV2", + "constructor": { + "docs": [ + "- `min_energy_for_propose` - the minimum energy required for submitting a proposal", + "- `min_fee_for_propose` - the minimum fee required for submitting a proposal", + "- `quorum` - the minimum number of (`votes` minus `downvotes`) at the end of voting period ", + "- `maxActionsPerProposal` - Maximum number of actions (transfers and/or smart contract calls) that a proposal may have ", + "- `votingDelayInBlocks` - Number of blocks to wait after a block is proposed before being able to vote/downvote that proposal", + "- `votingPeriodInBlocks` - Number of blocks the voting period lasts (voting delay does not count towards this) ", + "- `lockTimeAfterVotingEndsInBlocks` - Number of blocks to wait before a successful proposal can be executed " + ], + "inputs": [ + { + "name": "min_energy_for_propose", + "type": "BigUint" + }, + { + "name": "min_fee_for_propose", + "type": "BigUint" + }, + { + "name": "quorum_percentage", + "type": "BigUint" + }, + { + "name": "voting_delay_in_blocks", + "type": "u64" + }, + { + "name": "voting_period_in_blocks", + "type": "u64" + }, + { + "name": "withdraw_percentage_defeated", + "type": "u64" + }, + { + "name": "energy_factory_address", + "type": "Address" + }, + { + "name": "fees_collector_address", + "type": "Address" + }, + { + "name": "fee_token", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "docs": [ + "Propose a list of actions.", + "A maximum of MAX_GOVERNANCE_PROPOSAL_ACTIONS can be proposed at a time.", + "", + "An action has the following format:", + " - gas limit for action execution", + " - destination address", + " - a fee payment for proposal (if smaller than min_fee_for_propose, state: WaitForFee)", + " - endpoint to be called on the destination", + " - a vector of arguments for the endpoint, in the form of ManagedVec", + "", + "The proposer's energy is NOT automatically used for voting. A separate vote is needed.", + "", + "Returns the ID of the newly created proposal." + ], + "name": "propose", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "description", + "type": "bytes" + }, + { + "name": "actions", + "type": "variadic>>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Vote on a proposal. The voting power depends on the user's energy." + ], + "name": "vote", + "mutability": "mutable", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + }, + { + "name": "vote", + "type": "VoteType" + } + ], + "outputs": [] + }, + { + "docs": [ + "Cancel a proposed action. This can be done:", + "- by the proposer, at any time", + "- by anyone, if the proposal was defeated" + ], + "name": "cancel", + "mutability": "mutable", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "When a proposal was defeated, the proposer can withdraw", + "a part of the FEE." + ], + "name": "withdrawDeposit", + "mutability": "mutable", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "changeMinEnergyForProposal", + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "changeMinFeeForProposal", + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "changeQuorum", + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "changeVotingDelayInBlocks", + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "changeVotingPeriodInBlocks", + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "getMinEnergyForPropose", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getMinFeeForPropose", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getQuorum", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getVotingDelayInBlocks", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getVotingPeriodInBlocks", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getFeeTokenId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "getWithdrawPercentageDefeated", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getProposals", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ] + }, + { + "name": "getUserVotedProposals", + "mutability": "readonly", + "inputs": [ + { + "name": "user", + "type": "Address" + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ] + }, + { + "name": "getProposalVotes", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "ProposalVotes" + } + ] + }, + { + "name": "getProposalStatus", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "GovernanceProposalStatus" + } + ] + }, + { + "name": "getCurrentQuorum", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getProposer", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional
", + "multi_result": true + } + ] + }, + { + "name": "getProposalDescription", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional", + "multi_result": true + } + ] + }, + { + "name": "getProposalActions", + "mutability": "readonly", + "inputs": [ + { + "name": "proposal_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "getFeesCollectorAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "setEnergyFactoryAddress", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "sc_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "getEnergyFactoryAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "addAdmin", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "removeAdmin", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "updateOwnerOrAdmin", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "previous_owner", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "getPermissions", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + } + ], + "events": [ + { + "identifier": "proposalCreated", + "inputs": [ + { + "name": "proposal_id", + "type": "u32", + "indexed": true + }, + { + "name": "proposer", + "type": "Address", + "indexed": true + }, + { + "name": "start_block", + "type": "u64", + "indexed": true + }, + { + "name": "proposal", + "type": "GovernanceProposal" + } + ] + }, + { + "identifier": "upVoteCast", + "inputs": [ + { + "name": "up_voter", + "type": "Address", + "indexed": true + }, + { + "name": "proposal_id", + "type": "u32", + "indexed": true + }, + { + "name": "nr_votes", + "type": "BigUint" + } + ] + }, + { + "identifier": "downVoteCast", + "inputs": [ + { + "name": "down_voter", + "type": "Address", + "indexed": true + }, + { + "name": "proposal_id", + "type": "u32", + "indexed": true + }, + { + "name": "nr_downvotes", + "type": "BigUint" + } + ] + }, + { + "identifier": "downVetoVoteCast", + "inputs": [ + { + "name": "down_veto_voter", + "type": "Address", + "indexed": true + }, + { + "name": "proposal_id", + "type": "u32", + "indexed": true + }, + { + "name": "nr_downvotes", + "type": "BigUint" + } + ] + }, + { + "identifier": "abstainVoteCast", + "inputs": [ + { + "name": "abstain_voter", + "type": "Address", + "indexed": true + }, + { + "name": "proposal_id", + "type": "u32", + "indexed": true + }, + { + "name": "nr_downvotes", + "type": "BigUint" + } + ] + }, + { + "identifier": "proposalCanceled", + "inputs": [ + { + "name": "proposal_id", + "type": "u32", + "indexed": true + } + ] + }, + { + "identifier": "proposalWithdrawAfterDefeated", + "inputs": [ + { + "name": "proposal_id", + "type": "u32", + "indexed": true + } + ] + } + ], + "hasCallback": false, + "types": { + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "GovernanceAction": { + "type": "struct", + "fields": [ + { + "name": "gas_limit", + "type": "u64" + }, + { + "name": "dest_address", + "type": "Address" + }, + { + "name": "function_name", + "type": "bytes" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + "GovernanceProposal": { + "type": "struct", + "fields": [ + { + "name": "proposal_id", + "type": "u32" + }, + { + "name": "proposer", + "type": "Address" + }, + { + "name": "actions", + "type": "List" + }, + { + "name": "description", + "type": "bytes" + }, + { + "name": "fee_payment", + "type": "EsdtTokenPayment" + }, + { + "name": "minimum_quorum", + "type": "BigUint" + }, + { + "name": "voting_delay_in_blocks", + "type": "u64" + }, + { + "name": "voting_period_in_blocks", + "type": "u64" + }, + { + "name": "withdraw_percentage_defeated", + "type": "u64" + }, + { + "name": "total_energy", + "type": "BigUint" + }, + { + "name": "proposal_start_block", + "type": "u64" + } + ] + }, + "GovernanceProposalStatus": { + "type": "enum", + "variants": [ + { + "name": "None", + "discriminant": 0 + }, + { + "name": "Pending", + "discriminant": 1 + }, + { + "name": "Active", + "discriminant": 2 + }, + { + "name": "Defeated", + "discriminant": 3 + }, + { + "name": "DefeatedWithVeto", + "discriminant": 4 + }, + { + "name": "Succeeded", + "discriminant": 5 + } + ] + }, + "ProposalVotes": { + "type": "struct", + "fields": [ + { + "name": "up_votes", + "type": "BigUint" + }, + { + "name": "down_votes", + "type": "BigUint" + }, + { + "name": "down_veto_votes", + "type": "BigUint" + }, + { + "name": "abstain_votes", + "type": "BigUint" + }, + { + "name": "quorum", + "type": "BigUint" + } + ] + }, + "VoteType": { + "type": "enum", + "variants": [ + { + "name": "UpVote", + "discriminant": 0 + }, + { + "name": "DownVote", + "discriminant": 1 + }, + { + "name": "DownVetoVote", + "discriminant": 2 + }, + { + "name": "AbstainVote", + "discriminant": 3 + } + ] + } + } +} diff --git a/src/config/default.json b/src/config/default.json index 7cdbb699d..cd2521b2e 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -505,7 +505,8 @@ "energyUpdate": "./src/abis/energy-update.abi.json", "tokenUnstake": "./src/abis/token-unstake.abi.json", "lockedTokenWrapper": "./src/abis/locked-token-wrapper.abi.json", - "escrow": "./src/abis/lkmex-transfer.abi.json" + "escrow": "./src/abis/lkmex-transfer.abi.json", + "governance": "./src/abis/governance-v2.abi.json" }, "cron": { "transactionCollectorMaxHyperblocks": 10, diff --git a/src/config/devnet.json b/src/config/devnet.json index 7e30a7bb8..bd7de9e3d 100644 --- a/src/config/devnet.json +++ b/src/config/devnet.json @@ -48,7 +48,13 @@ "energyUpdate": "erd1qqqqqqqqqqqqqpgqqns0u3hw0e3j0km9h77emuear4xq7k7fd8ss0cwgja", "tokenUnstake": "erd1qqqqqqqqqqqqqpgqnysvq99c2t4a9pvvv22elnl6p73el8vw0n4spyfv7p", "lockedTokenWrapper": "erd1qqqqqqqqqqqqqpgq9ej9vcnr38l69rgkc735kgv0qlu2ptrsd8ssu9rwtu", - "escrow": "erd1qqqqqqqqqqqqqpgqz0wkk0j6y4h0mcxfxsg023j4x5sfgrmz0n4s4swp7a" + "escrow": "erd1qqqqqqqqqqqqqpgqz0wkk0j6y4h0mcxfxsg023j4x5sfgrmz0n4s4swp7a", + "governance": { + "energy": [ + "erd1qqqqqqqqqqqqqpgqu5u5kgmjm067dhwdfe4l3flwmadm2nqlczfskufug3" + ], + "token": [] + } }, "farms": { "v1.2": [], diff --git a/src/modules/farm/mocks/farm.v2.abi.service.mock.ts b/src/modules/farm/mocks/farm.v2.abi.service.mock.ts new file mode 100644 index 000000000..c4b3e89ff --- /dev/null +++ b/src/modules/farm/mocks/farm.v2.abi.service.mock.ts @@ -0,0 +1,54 @@ +import { FarmAbiServiceV2 } from '../v2/services/farm.v2.abi.service'; +import { FarmAbiServiceMock } from './farm.abi.service.mock'; +import { IFarmAbiServiceV2 } from '../v2/services/interfaces'; +import { BoostedYieldsFactors } from '../models/farm.v2.model'; + +export class FarmAbiServiceMockV2 + extends FarmAbiServiceMock + implements IFarmAbiServiceV2 +{ + async lastUndistributedBoostedRewardsCollectWeek( + farmAddress: string, + ): Promise { + return 1; + } + async undistributedBoostedRewards(farmAddress: string): Promise { + return '5000'; + } + async remainingBoostedRewardsToDistribute( + farmAddress: string, + week: number, + ): Promise { + return '1000'; + } + + accumulatedRewardsForWeek(scAddress: string, week: number): Promise { + throw new Error('Method not implemented.'); + } + + boostedYieldsFactors(farmAddress: string): Promise { + throw new Error('Method not implemented.'); + } + + boostedYieldsRewardsPercenatage(farmAddress: string): Promise { + throw new Error('Method not implemented.'); + } + + energyFactoryAddress(farmAddress: string): Promise { + throw new Error('Method not implemented.'); + } + + lockEpochs(farmAddress: string): Promise { + throw new Error('Method not implemented.'); + } + + lockingScAddress(farmAddress: string): Promise { + throw new Error('Method not implemented.'); + } +} + + +export const FarmAbiServiceProviderV2 = { + provide: FarmAbiServiceV2, + useClass: FarmAbiServiceMockV2, +}; diff --git a/src/modules/farm/models/farm.v2.model.ts b/src/modules/farm/models/farm.v2.model.ts index 997e43db8..0d14d880d 100644 --- a/src/modules/farm/models/farm.v2.model.ts +++ b/src/modules/farm/models/farm.v2.model.ts @@ -34,6 +34,8 @@ export class FarmModelV2 extends BaseFarmModel { @Field() undistributedBoostedRewards: string; @Field() + undistributedBoostedRewardsClaimed: string; + @Field() energyFactoryAddress: string; @Field() rewardType: FarmRewardType; diff --git a/src/modules/farm/specs/farm.v2.compute.service.spec.ts b/src/modules/farm/specs/farm.v2.compute.service.spec.ts new file mode 100644 index 000000000..c0903518c --- /dev/null +++ b/src/modules/farm/specs/farm.v2.compute.service.spec.ts @@ -0,0 +1,81 @@ +import { BigNumber } from 'bignumber.js'; +import { Test, TestingModule } from '@nestjs/testing'; +import { FarmComputeServiceV2 } from '../v2/services/farm.v2.compute.service'; +import { CommonAppModule } from '../../../common.app.module'; +import { CachingModule } from '../../../services/caching/cache.module'; +import { FarmAbiServiceProviderV2 } from '../mocks/farm.v2.abi.service.mock'; +import { FarmServiceV2 } from '../v2/services/farm.v2.service'; +import { MXDataApiServiceProvider } from '../../../services/multiversx-communication/mx.data.api.service.mock'; +import { WrapAbiServiceProvider } from '../../wrapping/mocks/wrap.abi.service.mock'; +import { RouterAbiServiceProvider } from '../../router/mocks/router.abi.service.mock'; +import { TokenComputeService } from '../../tokens/services/token.compute.service'; +import { TokenGetterServiceProvider } from '../../tokens/mocks/token.getter.service.mock'; +import { PairComputeServiceProvider } from '../../pair/mocks/pair.compute.service.mock'; +import { PairAbiServiceProvider } from '../../pair/mocks/pair.abi.service.mock'; +import { PairService } from '../../pair/services/pair.service'; +import { ContextGetterServiceProvider } from '../../../services/context/mocks/context.getter.service.mock'; +import { MXApiServiceProvider } from '../../../services/multiversx-communication/mx.api.service.mock'; +import { + WeekTimekeepingAbiServiceProvider, +} from '../../../submodules/week-timekeeping/mocks/week.timekeeping.abi.service.mock'; +import { + WeekTimekeepingComputeService, +} from '../../../submodules/week-timekeeping/services/week-timekeeping.compute.service'; +import { + WeeklyRewardsSplittingAbiServiceProvider, +} from '../../../submodules/weekly-rewards-splitting/mocks/weekly.rewards.splitting.abi.mock'; +import { + WeeklyRewardsSplittingComputeService, +} from '../../../submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.compute.service'; +import { EnergyAbiServiceProvider } from '../../energy/mocks/energy.abi.service.mock'; + +describe('FarmServiceV2', () => { + let module: TestingModule; + + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [CommonAppModule, CachingModule], + providers: [ + MXApiServiceProvider, + ContextGetterServiceProvider, + PairService, + PairAbiServiceProvider, + PairComputeServiceProvider, + TokenGetterServiceProvider, + TokenComputeService, + RouterAbiServiceProvider, + WrapAbiServiceProvider, + MXDataApiServiceProvider, + WeekTimekeepingAbiServiceProvider, + WeekTimekeepingComputeService, + WeeklyRewardsSplittingAbiServiceProvider, + EnergyAbiServiceProvider, + WeeklyRewardsSplittingComputeService, + FarmComputeServiceV2, + FarmAbiServiceProviderV2, + FarmServiceV2, + ], + }).compile(); + }); + it("should correctly calculate total rewards", async () => { + const service = module.get( + FarmComputeServiceV2, + ); + const result = await service.undistributedBoostedRewards('erd18h5dulxp5zdp80qjndd2w25kufx0rm5yqd2h7ajrfucjhr82y8vqyq0hye', 10); + const expectedTotal = new BigNumber('5000').plus('4000').integerValue().toFixed(); // 4 weeks * 1000 + expect(result).toEqual(expectedTotal); + // expect(mockFarmAbi.undistributedBoostedRewards).toHaveBeenCalled(); + // expect(mockFarmAbi.lastUndistributedBoostedRewardsCollectWeek).toHaveBeenCalled(); + // expect(mockFarmAbi.remainingBoostedRewardsToDistribute).toHaveBeenCalledTimes(4); + }); + // it("should return undistributedBoostedRewards if firstWeek > lastWeek", async () => { + // const service = module.get( + // FarmComputeServiceV2, + // ); + // const result = await service.computeUndistributedBoostedRewards('erd18h5dulxp5zdp80qjndd2w25kufx0rm5yqd2h7ajrfucjhr82y8vqyq0hye', 5); + // expect(result).toEqual('5000'); + // // expect(mockFarmAbi.undistributedBoostedRewards).toHaveBeenCalled(); + // // expect(mockFarmAbi.lastUndistributedBoostedRewardsCollectWeek).toHaveBeenCalled(); + // }, 10000); +}); + diff --git a/src/modules/farm/v2/farm.v2.resolver.ts b/src/modules/farm/v2/farm.v2.resolver.ts index fb35060c5..2850404ed 100644 --- a/src/modules/farm/v2/farm.v2.resolver.ts +++ b/src/modules/farm/v2/farm.v2.resolver.ts @@ -2,12 +2,16 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; import { BoostedYieldsFactors, FarmModelV2 } from '../models/farm.v2.model'; import { FarmResolver } from '../base-module/farm.resolver'; import { FarmServiceV2 } from './services/farm.v2.service'; -import { GlobalInfoByWeekModel } from '../../../submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; +import { + GlobalInfoByWeekModel, +} from '../../../submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; import { WeekTimekeepingModel } from '../../../submodules/week-timekeeping/models/week-timekeeping.model'; import { FarmComputeServiceV2 } from './services/farm.v2.compute.service'; import { constantsConfig } from '../../../config'; import { WeekTimekeepingAbiService } from 'src/submodules/week-timekeeping/services/week-timekeeping.abi.service'; -import { WeeklyRewardsSplittingAbiService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; +import { + WeeklyRewardsSplittingAbiService, +} from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; import { FarmAbiServiceV2 } from './services/farm.v2.abi.service'; @Resolver(() => FarmModelV2) @@ -110,6 +114,16 @@ export class FarmResolverV2 extends FarmResolver { @ResolveField() async undistributedBoostedRewards( @Parent() parent: FarmModelV2, + ): Promise { + const currentWeek = await this.weekTimekeepingAbi.currentWeek( + parent.address, + ); + return this.farmCompute.undistributedBoostedRewards(parent.address, currentWeek); + } + + @ResolveField() + async undistributedBoostedRewardsClaimed( + @Parent() parent: FarmModelV2, ): Promise { return this.farmAbi.undistributedBoostedRewards(parent.address); } diff --git a/src/modules/farm/v2/services/farm.v2.abi.service.ts b/src/modules/farm/v2/services/farm.v2.abi.service.ts index ca380e81f..80b467036 100644 --- a/src/modules/farm/v2/services/farm.v2.abi.service.ts +++ b/src/modules/farm/v2/services/farm.v2.abi.service.ts @@ -184,6 +184,23 @@ export class FarmAbiServiceV2 return response.firstValue.valueOf().toFixed(); } + @ErrorLoggerAsync({ + className: FarmAbiServiceV2.name, + logArgs: true, + }) + @GetOrSetCache({ + baseKey: 'farm', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async lastUndistributedBoostedRewardsCollectWeek( + farmAddress: string, + ): Promise { + return this.gatewayService.getSCStorageKey(farmAddress, + 'lastUndistributedBoostedRewardsCollectWeek' + ); + } + @ErrorLoggerAsync({ className: FarmAbiServiceV2.name, logArgs: true, diff --git a/src/modules/farm/v2/services/farm.v2.compute.service.ts b/src/modules/farm/v2/services/farm.v2.compute.service.ts index 2b18a56ff..18ff59f1e 100644 --- a/src/modules/farm/v2/services/farm.v2.compute.service.ts +++ b/src/modules/farm/v2/services/farm.v2.compute.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, forwardRef } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { FarmComputeService } from '../../base-module/services/farm.compute.service'; import BigNumber from 'bignumber.js'; import { EsdtTokenPayment } from '../../../../models/esdtTokenPayment.model'; @@ -8,8 +8,12 @@ import { constantsConfig } from '../../../../config'; import { CalculateRewardsArgs } from '../../models/farm.args'; import { PairService } from '../../../pair/services/pair.service'; import { ContextGetterService } from '../../../../services/context/context.getter.service'; -import { WeekTimekeepingComputeService } from 'src/submodules/week-timekeeping/services/week-timekeeping.compute.service'; -import { WeeklyRewardsSplittingAbiService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; +import { + WeekTimekeepingComputeService, +} from 'src/submodules/week-timekeeping/services/week-timekeeping.compute.service'; +import { + WeeklyRewardsSplittingAbiService, +} from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; import { FarmAbiServiceV2 } from './farm.v2.abi.service'; import { FarmServiceV2 } from './farm.v2.service'; import { ErrorLoggerAsync } from 'src/helpers/decorators/error.logger'; @@ -17,7 +21,9 @@ import { GetOrSetCache } from 'src/helpers/decorators/caching.decorator'; import { CacheTtlInfo } from 'src/services/caching/cache.ttl.info'; import { CachingService } from 'src/services/caching/cache.service'; import { TokenDistributionModel } from 'src/submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; -import { WeeklyRewardsSplittingComputeService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.compute.service'; +import { + WeeklyRewardsSplittingComputeService, +} from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.compute.service'; import { IFarmComputeServiceV2 } from './interfaces'; @Injectable() @@ -457,6 +463,63 @@ export class FarmComputeServiceV2 .toFixed(); } + @ErrorLoggerAsync({ + className: FarmComputeServiceV2.name, + logArgs: true, + }) + @GetOrSetCache({ + baseKey: 'farm', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async undistributedBoostedRewards( + scAddress: string, + currentWeek: number, + ): Promise { + const amount = await this.undistributedBoostedRewardsRaw( + scAddress, + currentWeek, + ); + return amount.integerValue().toFixed(); + } + + async undistributedBoostedRewardsRaw( + scAddress: string, + currentWeek: number, + ): Promise { + const [ + undistributedBoostedRewards, + lastUndistributedBoostedRewardsCollectWeek, + ] = await Promise.all([ + this.farmAbi.undistributedBoostedRewards(scAddress), + this.farmAbi.lastUndistributedBoostedRewardsCollectWeek( + scAddress, + ), + ]); + + const firstWeek = lastUndistributedBoostedRewardsCollectWeek + 1; + const lastWeek = currentWeek - constantsConfig.USER_MAX_CLAIM_WEEKS - 1; + if (firstWeek > lastWeek) { + return new BigNumber(undistributedBoostedRewards); + } + const promises = [] + for (let week = firstWeek; week <= lastWeek; week++) { + promises.push( + this.farmAbi.remainingBoostedRewardsToDistribute( + scAddress, + week, + ) + ) + } + const remainingRewards = await Promise.all(promises); + const totalRemainingRewards = remainingRewards.reduce((acc, curr) => { + return new BigNumber(acc).plus(curr); + }); + return new BigNumber(undistributedBoostedRewards) + .plus(totalRemainingRewards); + + } + async computeBlocksInWeek( scAddress: string, week: number, diff --git a/src/modules/farm/v2/services/interfaces.ts b/src/modules/farm/v2/services/interfaces.ts index 49677d289..5dff92749 100644 --- a/src/modules/farm/v2/services/interfaces.ts +++ b/src/modules/farm/v2/services/interfaces.ts @@ -1,8 +1,5 @@ import { TokenDistributionModel } from 'src/submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; -import { - IFarmAbiService, - IFarmComputeService, -} from '../../base-module/services/interfaces'; +import { IFarmAbiService, IFarmComputeService } from '../../base-module/services/interfaces'; import { BoostedYieldsFactors } from '../../models/farm.v2.model'; import { EsdtTokenPayment } from '../../../../models/esdtTokenPayment.model'; @@ -14,6 +11,9 @@ export interface IFarmAbiServiceV2 extends IFarmAbiService { farmAddress: string, week: number, ): Promise; + lastUndistributedBoostedRewardsCollectWeek( + farmAddress: string, + ): Promise; undistributedBoostedRewards(farmAddress: string): Promise; boostedYieldsFactors(farmAddress: string): Promise; accumulatedRewardsForWeek(scAddress: string, week: number): Promise; diff --git a/src/modules/governance/governance.contract.resolver.ts b/src/modules/governance/governance.contract.resolver.ts new file mode 100644 index 000000000..1dde7375f --- /dev/null +++ b/src/modules/governance/governance.contract.resolver.ts @@ -0,0 +1,62 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { GovernanceAbiService } from './services/governance.abi.service'; +import { GovernanceContract } from './models/governance.contract.model'; +import { GovernanceProposal } from './models/governance.proposal.model'; + +@Resolver(() => GovernanceContract) +export class GovernanceContractResolver { + constructor( + private readonly governanceAbi: GovernanceAbiService, + ) { + } + + @ResolveField() + async minEnergyForPropose(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.minEnergyForPropose(governanceContract.address); + } + + @ResolveField() + async minFeeForPropose(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.minFeeForPropose(governanceContract.address); + } + + @ResolveField() + async quorum(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.quorum(governanceContract.address); + } + + @ResolveField() + async votingDelayInBlocks(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.votingDelayInBlocks(governanceContract.address); + } + + @ResolveField() + async votingPeriodInBlocks(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.votingPeriodInBlocks(governanceContract.address); + } + + @ResolveField() + async feeTokenId(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.feeTokenId(governanceContract.address); + } + + @ResolveField() + async withdrawPercentageDefeated(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.withdrawPercentageDefeated(governanceContract.address); + } + + @ResolveField(() => [GovernanceProposal]) + async proposals(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.proposals(governanceContract.address); + } + + @ResolveField() + async feesCollectorAddress(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.feesCollectorAddress(governanceContract.address); + } + + @ResolveField() + async energyFactoryAddress(@Parent() governanceContract: GovernanceContract): Promise { + return this.governanceAbi.energyFactoryAddress(governanceContract.address); + } +} diff --git a/src/modules/governance/governance.module.ts b/src/modules/governance/governance.module.ts new file mode 100644 index 000000000..906561dcb --- /dev/null +++ b/src/modules/governance/governance.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { CommonAppModule } from 'src/common.app.module'; +import { CachingModule } from 'src/services/caching/cache.module'; +import { ContextModule } from 'src/services/context/context.module'; +import { MXCommunicationModule } from 'src/services/multiversx-communication/mx.communication.module'; +import { TokenModule } from '../tokens/token.module'; +import { EnergyModule } from '../energy/energy.module'; +import { GovernanceAbiService } from './services/governance.abi.service'; +import { GovernanceQueryResolver } from './governance.query.resolver'; +import { GovernanceContractResolver } from './governance.contract.resolver'; +import { GovernanceProposalResolver } from './governance.propose.resolver'; +import { GovernanceService } from './services/governance.service'; + + +@Module({ + imports: [ + CommonAppModule, + CachingModule, + MXCommunicationModule, + ContextModule, + TokenModule, + EnergyModule + ], + providers: [ + GovernanceService, + GovernanceAbiService, + // GovernanceSetterService, + // GovernanceComputeService, + // GovernanceTransactionService, + GovernanceQueryResolver, + GovernanceContractResolver, + GovernanceProposalResolver, + ], + exports: [ + GovernanceAbiService, + // GovernanceSetterService, + // GovernanceComputeService, + GovernanceService, + ], +}) +export class GovernanceModule {} diff --git a/src/modules/governance/governance.propose.resolver.ts b/src/modules/governance/governance.propose.resolver.ts new file mode 100644 index 000000000..9924e7e47 --- /dev/null +++ b/src/modules/governance/governance.propose.resolver.ts @@ -0,0 +1,37 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { GovernanceAbiService } from './services/governance.abi.service'; +import { GovernanceProposal, GovernanceProposalStatus } from './models/governance.proposal.model'; +import { ProposalVotes } from './models/proposal.votes.model'; +import { GovernanceService } from './services/governance.service'; +import { UseGuards } from '@nestjs/common'; +import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth.guard'; +import { UserAuthResult } from '../auth/user.auth.result'; +import { AuthUser } from '../auth/auth.user'; + +@Resolver(() => GovernanceProposal) +export class GovernanceProposalResolver { + constructor( + private readonly governanceAbi: GovernanceAbiService, + private readonly governanceService: GovernanceService, + ) { + } + + @ResolveField() + async status(@Parent() governanceProposal: GovernanceProposal): Promise { + return this.governanceAbi.proposalStatus(governanceProposal.contractAddress, governanceProposal.proposalId); + } + + @ResolveField() + async votes(@Parent() governanceProposal: GovernanceProposal): Promise { + return this.governanceAbi.proposalVotes(governanceProposal.contractAddress, governanceProposal.proposalId); + } + + @UseGuards(JwtOrNativeAuthGuard) + @ResolveField() + async hasVoted( + @AuthUser() user: UserAuthResult, + @Parent() governanceProposal: GovernanceProposal + ): Promise { + return this.governanceService.hasUserVoted(governanceProposal.contractAddress, governanceProposal.proposalId, user.address); + } +} diff --git a/src/modules/governance/governance.query.resolver.ts b/src/modules/governance/governance.query.resolver.ts new file mode 100644 index 000000000..0cc03d6e2 --- /dev/null +++ b/src/modules/governance/governance.query.resolver.ts @@ -0,0 +1,19 @@ +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { GovernanceContractsFiltersArgs } from './models/contracts.filter.args'; +import { GovernanceService } from './services/governance.service'; +import { GovernanceContract } from './models/governance.contract.model'; + +@Resolver() +export class GovernanceQueryResolver { + constructor( + private readonly governanceService: GovernanceService, + ) { + } + + @Query(() => [GovernanceContract]) + async governanceContracts( + @Args() filters: GovernanceContractsFiltersArgs + ): Promise { + return this.governanceService.getGovernanceContracts(filters); + } +} diff --git a/src/modules/governance/models/contracts.filter.args.ts b/src/modules/governance/models/contracts.filter.args.ts new file mode 100644 index 000000000..70bf4de09 --- /dev/null +++ b/src/modules/governance/models/contracts.filter.args.ts @@ -0,0 +1,11 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +@ArgsType() +export class GovernanceContractsFiltersArgs { + @Field(() => [String], { nullable: true }) + identifiers: string; + @Field(() => [String], { nullable: true }) + contracts: string; + @Field({ nullable: true }) + type: string; +} diff --git a/src/modules/governance/models/governance.action.model.ts b/src/modules/governance/models/governance.action.model.ts new file mode 100644 index 000000000..e65f7203f --- /dev/null +++ b/src/modules/governance/models/governance.action.model.ts @@ -0,0 +1,17 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class GovernanceAction { + @Field(() => Int) + gasLimit: number; + @Field() + destAddress: string; + @Field() + functionName: string; + @Field( () => [String]) + arguments: string[]; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/modules/governance/models/governance.contract.model.ts b/src/modules/governance/models/governance.contract.model.ts new file mode 100644 index 000000000..89c1258d1 --- /dev/null +++ b/src/modules/governance/models/governance.contract.model.ts @@ -0,0 +1,37 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { GovernanceProposal } from './governance.proposal.model'; + +export enum GovernanceType { + ENERGY = 'energy', + TOKEN = 'token', +} + +@ObjectType() +export class GovernanceContract { + @Field() + address: string; + @Field() + minEnergyForPropose: string; + @Field() + minFeeForPropose: string; + @Field() + quorum: string; + @Field(() => Int) + votingDelayInBlocks: number; + @Field(() => Int) + votingPeriodInBlocks: number; + @Field() + feeTokenId: string; + @Field(() => Int) + withdrawPercentageDefeated: number; + @Field(() => [GovernanceProposal]) + proposals: GovernanceProposal[]; + @Field() + feesCollectorAddress: string; + @Field() + energyFactoryAddress: string; + + constructor(init: Partial) { + Object.assign(this, init); + } +} diff --git a/src/modules/governance/models/governance.proposal.model.ts b/src/modules/governance/models/governance.proposal.model.ts new file mode 100644 index 000000000..402931b79 --- /dev/null +++ b/src/modules/governance/models/governance.proposal.model.ts @@ -0,0 +1,67 @@ +import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { GovernanceAction } from './governance.action.model'; +import { EsdtTokenPaymentModel } from '../../tokens/models/esdt.token.payment.model'; +import { ProposalVotes } from './proposal.votes.model'; //Assuming you will create GovernanceAction model separately + +export enum GovernanceProposalStatus { + None ='None', + Pending ='Pending', + Active ='Active', + Defeated ='Defeated', + DefeatedWithVeto ='DefeatedWithVeto', + Succeeded ='Succeeded', +} + +registerEnumType(GovernanceProposalStatus, { name: 'GovernanceProposalStatus' }); + +@ObjectType() +export class Description { + @Field() + title: string; + @Field() + hash: string; + @Field(() => Int) + strapiId: number; + + constructor(init: Partial) { + Object.assign(this, init); + } +} + +@ObjectType() +export class GovernanceProposal { + @Field() + contractAddress: string; + @Field() + proposalId: number; + @Field() + proposer: string; + @Field(() => [GovernanceAction]) + actions: GovernanceAction[]; + @Field( () => Description) + description: Description; + @Field(() => EsdtTokenPaymentModel) + feePayment: EsdtTokenPaymentModel; + @Field() + minimumQuorum: string; + @Field(() => Int) + votingDelayInBlocks: number; + @Field(() => Int) + votingPeriodInBlocks: number; + @Field(() => Int) + withdrawPercentageDefeated: number; + @Field() + totalEnergy: string; + @Field(() => Int) + proposalStartBlock: number; + @Field() + status: GovernanceProposalStatus; + @Field( () => ProposalVotes ) + votes: ProposalVotes; + @Field() + hasVoted?: boolean; + + constructor(init: Partial) { + Object.assign(this, init); + } +} diff --git a/src/modules/governance/models/proposal.votes.model.ts b/src/modules/governance/models/proposal.votes.model.ts new file mode 100644 index 000000000..4b6342ff6 --- /dev/null +++ b/src/modules/governance/models/proposal.votes.model.ts @@ -0,0 +1,29 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class ProposalVotes { + @Field() + upVotes: string; + @Field() + downVotes: string; + @Field() + downVetoVotes: string; + @Field() + abstainVotes: string; + @Field() + quorum: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static default(): ProposalVotes { + return new ProposalVotes({ + upVotes: '0', + downVotes: '0', + downVetoVotes: '0', + abstainVotes: '0', + quorum: '0', + }); + } +} diff --git a/src/modules/governance/services/governance.abi.service.ts b/src/modules/governance/services/governance.abi.service.ts new file mode 100644 index 000000000..64cdaa7b7 --- /dev/null +++ b/src/modules/governance/services/governance.abi.service.ts @@ -0,0 +1,292 @@ +import { Injectable } from '@nestjs/common'; +import { MXProxyService } from 'src/services/multiversx-communication/mx.proxy.service'; +import { GenericAbiService } from 'src/services/generics/generic.abi.service'; +import { ErrorLoggerAsync } from 'src/helpers/decorators/error.logger'; +import { ProposalVotes } from '../models/proposal.votes.model'; +import { Description, GovernanceProposal, GovernanceProposalStatus } from '../models/governance.proposal.model'; +import { GovernanceAction } from '../models/governance.action.model'; +import { EsdtTokenPaymentModel } from '../../tokens/models/esdt.token.payment.model'; +import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { toGovernanceProposalStatus } from '../../../utils/governance'; +import { GetOrSetCache } from '../../../helpers/decorators/caching.decorator'; +import { CacheTtlInfo } from '../../../services/caching/cache.ttl.info'; + +@Injectable() +export class GovernanceAbiService + extends GenericAbiService +{ + constructor( + protected readonly mxProxy: MXProxyService, + ) { + super(mxProxy); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async minEnergyForPropose(scAddress: string): Promise { + return await this.minEnergyForProposeRaw(scAddress); + } + + async minEnergyForProposeRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getMinEnergyForPropose(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toFixed(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async minFeeForPropose(scAddress: string): Promise { + return await this.minFeeForProposeRaw(scAddress); + } + + async minFeeForProposeRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getMinFeeForPropose(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toFixed(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async quorum(scAddress: string): Promise { + return await this.quorumRaw(scAddress); + } + + async quorumRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getQuorum(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toFixed(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async votingDelayInBlocks(scAddress: string): Promise { + return await this.votingDelayInBlocksRaw(scAddress); + } + + async votingDelayInBlocksRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getVotingDelayInBlocks(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toNumber(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async votingPeriodInBlocks(scAddress: string): Promise { + return await this.votingPeriodInBlocksRaw(scAddress); + } + + async votingPeriodInBlocksRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getVotingPeriodInBlocks(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toNumber(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async feeTokenId(scAddress: string): Promise { + return await this.feeTokenIdRaw(scAddress); + } + + async feeTokenIdRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getFeeTokenId(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async withdrawPercentageDefeated(scAddress: string): Promise { + return await this.withdrawPercentageDefeatedRaw(scAddress); + } + + async withdrawPercentageDefeatedRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getWithdrawPercentageDefeated(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().toNumber(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async proposals(scAddress: string): Promise { + return await this.proposalsRaw(scAddress); + } + + async proposalsRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methodsExplicit.getProposals(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().map((proposal: any) => { + const actions = proposal.actions?.map((action: any) => { + return new GovernanceAction({ + arguments: action.arguments.toString().split(','), + destAddress: action.dest_address.bech32(), + functionName: action.function_name.toString(), + gasLimit: action.gas_limit.toNumber(), + }); + }); + return new GovernanceProposal({ + contractAddress: scAddress, + proposalId: proposal.proposal_id.toNumber(), + proposer: proposal.proposer.bech32(), + actions, + description: new Description(JSON.parse(proposal.description.toString())), + feePayment: new EsdtTokenPaymentModel( + EsdtTokenPayment.fromDecodedAttributes(proposal.fee_payment) + ), + proposalStartBlock: proposal.proposal_start_block.toNumber(), + minimumQuorum: proposal.minimum_quorum.toNumber(), + totalEnergy: proposal.total_energy.toFixed(), + votingDelayInBlocks: proposal.voting_delay_in_blocks.toNumber(), + votingPeriodInBlocks: proposal.voting_period_in_blocks.toNumber(), + withdrawPercentageDefeated: proposal.withdraw_percentage_defeated.toNumber(), + }); + }); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async userVotedProposals(scAddress: string, userAddress: string): Promise { + return await this.userVotedProposalsRaw(scAddress, userAddress); + } + + async userVotedProposalsRaw(scAddress: string, userAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getUserVotedProposals([userAddress]); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().map((proposalId: any) => proposalId.toNumber()); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async proposalVotes(scAddress: string, proposalId: number): Promise { + return await this.proposalVotesRaw(scAddress, proposalId); + } + + async proposalVotesRaw(scAddress: string, proposalId: number): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getProposalVotes([proposalId]); + const response = await this.getGenericData(interaction); + + if (!response.firstValue) { + return ProposalVotes.default(); + } + const votes = response.firstValue.valueOf(); + return new ProposalVotes({ + upVotes: votes.up_votes.toFixed(), + downVotes: votes.down_votes.toFixed(), + downVetoVotes: votes.down_veto_votes.toFixed(), + abstainVotes: votes.abstain_votes.toFixed(), + quorum: votes.quorum.toFixed() + }); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async proposalStatus(scAddress: string, proposalId: number): Promise { + return await this.proposalStatusRaw(scAddress, proposalId); + } + + async proposalStatusRaw(scAddress: string, proposalId: number): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getProposalStatus([proposalId]); + const response = await this.getGenericData(interaction); + + return toGovernanceProposalStatus(response.firstValue.valueOf().name); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async feesCollectorAddress(scAddress: string): Promise { + return await this.feesCollectorAddressRaw(scAddress); + } + + async feesCollectorAddressRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getFeesCollectorAddress(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().bech32(); + } + + @ErrorLoggerAsync({className: GovernanceAbiService.name}) + @GetOrSetCache({ + baseKey: 'governance', + remoteTtl: CacheTtlInfo.ContractState.remoteTtl, + localTtl: CacheTtlInfo.ContractState.localTtl, + }) + async energyFactoryAddress(scAddress: string): Promise { + return await this.energyFactoryAddressRaw(scAddress); + } + + async energyFactoryAddressRaw(scAddress: string): Promise { + const contract = await this.mxProxy.getGovernanceSmartContract(scAddress); + const interaction = contract.methods.getEnergyFactoryAddress(); + const response = await this.getGenericData(interaction); + + return response.firstValue.valueOf().bech32(); + } +} diff --git a/src/modules/governance/services/governance.service.ts b/src/modules/governance/services/governance.service.ts new file mode 100644 index 000000000..c71088b3b --- /dev/null +++ b/src/modules/governance/services/governance.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { GovernanceContract } from '../models/governance.contract.model'; +import { governanceContractsAddresses } from '../../../utils/governance'; +import { GovernanceContractsFiltersArgs } from '../models/contracts.filter.args'; +import { GovernanceAbiService } from './governance.abi.service'; + +@Injectable() +export class GovernanceService { + constructor( + private readonly governanceAbi: GovernanceAbiService, + ) { + } + async getGovernanceContracts(filters: GovernanceContractsFiltersArgs): Promise { + const governanceAddresses = governanceContractsAddresses(); + + const governance: GovernanceContract[] = []; + for (const address of governanceAddresses) { + governance.push( + new GovernanceContract({ + address, + }), + ); + } + + return governance; + } + + async hasUserVoted(contractAddress: string, proposalId: number, userAddress?: string): Promise { + if (!userAddress) { + return false; + } + + const userVotedProposals = await this.governanceAbi.userVotedProposals(contractAddress, userAddress); + return userVotedProposals.includes(proposalId); + } +} diff --git a/src/modules/locked-asset-factory/mocks/abi.locked.asset.service.mock.ts b/src/modules/locked-asset-factory/mocks/abi.locked.asset.service.mock.ts index 57b3f6f78..c36983a5b 100644 --- a/src/modules/locked-asset-factory/mocks/abi.locked.asset.service.mock.ts +++ b/src/modules/locked-asset-factory/mocks/abi.locked.asset.service.mock.ts @@ -1,4 +1,5 @@ import { UnlockMileStoneModel } from '../models/locked-asset.model'; +import { AbiLockedAssetService } from '../services/abi-locked-asset.service'; export class AbiLockedAssetServiceMock { async getLockedTokenID(): Promise { @@ -26,3 +27,8 @@ export class AbiLockedAssetServiceMock { return 2; } } + +export const AbiLockedAssetServiceProvider = { + provide: AbiLockedAssetService, + useClass: AbiLockedAssetServiceMock, +}; diff --git a/src/modules/locked-asset-factory/mocks/locked.asset.service.mock.ts b/src/modules/locked-asset-factory/mocks/locked.asset.service.mock.ts index 578e167e5..457c093f4 100644 --- a/src/modules/locked-asset-factory/mocks/locked.asset.service.mock.ts +++ b/src/modules/locked-asset-factory/mocks/locked.asset.service.mock.ts @@ -1,5 +1,12 @@ +import { LockedAssetService } from '../services/locked-asset.service'; + export class LockedAssetServiceMock { async getLockedTokenID(): Promise { return 'LKTOK-1234'; } } + +export const LockedAssetServiceProvider = { + provide: LockedAssetService, + useClass: LockedAssetServiceMock, +}; diff --git a/src/modules/price-discovery/mocks/price.discovery.abi.service.mock.ts b/src/modules/price-discovery/mocks/price.discovery.abi.service.mock.ts index ede3f2b62..37477e966 100644 --- a/src/modules/price-discovery/mocks/price.discovery.abi.service.mock.ts +++ b/src/modules/price-discovery/mocks/price.discovery.abi.service.mock.ts @@ -3,14 +3,14 @@ import { IPriceDiscoveryAbiService } from '../services/interfaces'; import { PriceDiscoveryAbiService } from '../services/price.discovery.abi.service'; export class PriceDiscoveryAbiServiceMock implements IPriceDiscoveryAbiService { - launchedTokenID(priceDiscoveryAddress: string): Promise { - throw new Error('Method not implemented.'); + async launchedTokenID(priceDiscoveryAddress: string): Promise { + return 'LTOK-123456'; } - acceptedTokenID(priceDiscoveryAddress: string): Promise { - throw new Error('Method not implemented.'); + async acceptedTokenID(priceDiscoveryAddress: string): Promise { + return 'ATOK-123456'; } async redeemTokenID(priceDiscoveryAddress: string): Promise { - return 'RTOK-1234'; + return 'RTOK-123456'; } launchedTokenAmount(priceDiscoveryAddress: string): Promise { throw new Error('Method not implemented.'); diff --git a/src/modules/price-discovery/services/price.discovery.compute.service.ts b/src/modules/price-discovery/services/price.discovery.compute.service.ts index 1e1d5361d..8cfd78b8a 100644 --- a/src/modules/price-discovery/services/price.discovery.compute.service.ts +++ b/src/modules/price-discovery/services/price.discovery.compute.service.ts @@ -10,6 +10,8 @@ import { CacheTtlInfo } from 'src/services/caching/cache.ttl.info'; import { IPriceDiscoveryComputeService } from './interfaces'; import { AnalyticsQueryService } from 'src/services/analytics/services/analytics.query.service'; import { HistoricDataModel } from 'src/modules/analytics/models/analytics.model'; +import { MXApiService } from 'src/services/multiversx-communication/mx.api.service'; +import moment from 'moment'; @Injectable() export class PriceDiscoveryComputeService @@ -20,6 +22,7 @@ export class PriceDiscoveryComputeService private readonly priceDiscoveryAbi: PriceDiscoveryAbiService, private readonly priceDiscoveryService: PriceDiscoveryService, private readonly analyticsQuery: AnalyticsQueryService, + private readonly apiService: MXApiService, ) {} @ErrorLoggerAsync({ @@ -229,10 +232,27 @@ export class PriceDiscoveryComputeService metric: string, interval: string, ): Promise { + const [startBlockNonce, endBlockNonce] = await Promise.all([ + this.priceDiscoveryAbi.startBlock(priceDiscoveryAddress), + this.priceDiscoveryAbi.endBlock(priceDiscoveryAddress), + ]); + + const [startBlock, endBlock] = await Promise.all([ + this.apiService.getBlockByNonce(1, startBlockNonce), + this.apiService.getBlockByNonce(1, endBlockNonce), + ]); + + const [startDate, endDate] = [ + startBlock.timestamp, + endBlock ? endBlock.timestamp : moment().unix(), + ]; + return await this.analyticsQuery.getPDCloseValues({ series: priceDiscoveryAddress, metric, timeBucket: interval, + startDate: moment.unix(startDate).toDate(), + endDate: moment.unix(endDate).toDate(), }); } } diff --git a/src/modules/rabbitmq/handlers/energy.handler.service.ts b/src/modules/rabbitmq/handlers/energy.handler.service.ts index 2e3a26d11..412735219 100644 --- a/src/modules/rabbitmq/handlers/energy.handler.service.ts +++ b/src/modules/rabbitmq/handlers/energy.handler.service.ts @@ -8,7 +8,6 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { scAddress } from 'src/config'; import { EnergySetterService } from 'src/modules/energy/services/energy.setter.service'; import { UserEnergyComputeService } from 'src/modules/user/services/userEnergy/user.energy.compute.service'; -import { UserEnergyGetterService } from 'src/modules/user/services/userEnergy/user.energy.getter.service'; import { UserEnergySetterService } from 'src/modules/user/services/userEnergy/user.energy.setter.service'; import { PUB_SUB } from 'src/services/redis.pubSub.module'; import { Logger } from 'winston'; @@ -17,7 +16,6 @@ import { Logger } from 'winston'; export class EnergyHandler { constructor( private readonly energySetter: EnergySetterService, - private readonly userEnergyGetter: UserEnergyGetterService, private readonly userEnergySetter: UserEnergySetterService, private readonly userEnergyCompute: UserEnergyComputeService, @Inject(PUB_SUB) private pubSub: RedisPubSub, @@ -34,20 +32,18 @@ export class EnergyHandler { ), ); - const activeFarms = await this.userEnergyGetter.getUserActiveFarmsV2( + const activeFarms = await this.userEnergyCompute.userActiveFarmsV2( caller.bech32(), ); const promises = activeFarms.map((farm) => this.userEnergyCompute.computeUserOutdatedContract( caller.bech32(), - event.newEnergyEntry.toJSON(), farm, ), ); promises.push( this.userEnergyCompute.computeUserOutdatedContract( caller.bech32(), - event.newEnergyEntry.toJSON(), scAddress.feesCollector, ), ); diff --git a/src/modules/rabbitmq/handlers/price.discovery.handler.service.ts b/src/modules/rabbitmq/handlers/price.discovery.handler.service.ts index bf43e83da..dd5b0dc5f 100644 --- a/src/modules/rabbitmq/handlers/price.discovery.handler.service.ts +++ b/src/modules/rabbitmq/handlers/price.discovery.handler.service.ts @@ -22,6 +22,10 @@ export class PriceDiscoveryEventHandler { async handleEvent( event: DepositEvent | WithdrawEvent, ): Promise<[any[], number]> { + const acceptedToken = await this.priceDiscoveryService.getAcceptedToken( + event.getAddress(), + ); + const [ priceDiscoveryAddress, launchedTokenAmount, @@ -31,13 +35,11 @@ export class PriceDiscoveryEventHandler { event.getAddress(), event.launchedTokenAmount.toFixed(), event.acceptedTokenAmount.toFixed(), - event.launchedTokenPrice, + event.launchedTokenPrice + .multipliedBy(`1e-${acceptedToken.decimals}`) + .toFixed(), ]; - const launchedToken = await this.priceDiscoveryService.getLaunchedToken( - priceDiscoveryAddress, - ); - let cacheKeys: string[] = await Promise.all([ this.priceDiscoverySetter.setLaunchedTokenAmount( priceDiscoveryAddress, @@ -49,9 +51,7 @@ export class PriceDiscoveryEventHandler { ), this.priceDiscoverySetter.setLaunchedTokenPrice( priceDiscoveryAddress, - launchedTokenPrice - .multipliedBy(`1e-${launchedToken.decimals}`) - .toFixed(), + launchedTokenPrice, ), this.priceDiscoverySetter.setCurrentPhase( priceDiscoveryAddress, diff --git a/src/modules/rabbitmq/handlers/weeklyRewardsSplitting.handler.service.ts b/src/modules/rabbitmq/handlers/weeklyRewardsSplitting.handler.service.ts index fb70d7efa..81d48323a 100644 --- a/src/modules/rabbitmq/handlers/weeklyRewardsSplitting.handler.service.ts +++ b/src/modules/rabbitmq/handlers/weeklyRewardsSplitting.handler.service.ts @@ -89,11 +89,9 @@ export class WeeklyRewardsSplittingHandlerService { ), ]); - const userEnergy = await this.energyAbi.energyEntryForUser(userAddress); const outdatedContract = await this.userEnergyCompute.computeUserOutdatedContract( userAddress, - userEnergy, contractAddress, ); diff --git a/src/modules/staking-proxy/mocks/staking.proxy.service.mock.ts b/src/modules/staking-proxy/mocks/staking.proxy.service.mock.ts index 357bd0407..7904d0b57 100644 --- a/src/modules/staking-proxy/mocks/staking.proxy.service.mock.ts +++ b/src/modules/staking-proxy/mocks/staking.proxy.service.mock.ts @@ -1 +1,8 @@ +import { StakingProxyService } from '../services/staking.proxy.service'; + export class StakingProxyServiceMock {} + +export const StakingProxyServiceProvider = { + provide: StakingProxyService, + useClass: StakingProxyServiceMock, +}; diff --git a/src/modules/staking/mocks/staking.service.mock.ts b/src/modules/staking/mocks/staking.service.mock.ts index 916b2062d..7040bd13e 100644 --- a/src/modules/staking/mocks/staking.service.mock.ts +++ b/src/modules/staking/mocks/staking.service.mock.ts @@ -1 +1,8 @@ +import { StakingService } from '../services/staking.service'; + export class StakingServiceMock {} + +export const StakingServiceProvider = { + provide: StakingService, + useClass: StakingServiceMock, +}; diff --git a/src/modules/user/services/metaEsdt.compute.service.ts b/src/modules/user/services/metaEsdt.compute.service.ts index d0447152c..5d2801977 100644 --- a/src/modules/user/services/metaEsdt.compute.service.ts +++ b/src/modules/user/services/metaEsdt.compute.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import BigNumber from 'bignumber.js'; import { scAddress } from '../../../config'; import { LockedAssetToken } from 'src/modules/tokens/models/lockedAssetToken.model'; @@ -52,18 +52,16 @@ import { UnbondFarmToken } from 'src/modules/tokens/models/unbondFarmToken.model import { LockedAssetGetterService } from 'src/modules/locked-asset-factory/services/locked.asset.getter.service'; import { FarmTokenAttributesModelV1_2 } from 'src/modules/farm/models/farmTokenAttributes.model'; import { LockedTokenWrapperService } from '../../locked-token-wrapper/services/locked-token-wrapper.service'; -import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; -import { Logger } from 'winston'; import { CachingService } from 'src/services/caching/cache.service'; import { CacheTtlInfo } from 'src/services/caching/cache.ttl.info'; import { TokenGetterService } from 'src/modules/tokens/services/token.getter.service'; import { PairComputeService } from 'src/modules/pair/services/pair.compute.service'; import { PriceDiscoveryAbiService } from 'src/modules/price-discovery/services/price.discovery.abi.service'; import { PriceDiscoveryComputeService } from 'src/modules/price-discovery/services/price.discovery.compute.service'; -import { LockedTokenWrapperAbiService } from 'src/modules/locked-token-wrapper/services/locked-token-wrapper.abi.service'; import { EnergyAbiService } from 'src/modules/energy/services/energy.abi.service'; import { StakingProxyAbiService } from 'src/modules/staking-proxy/services/staking.proxy.abi.service'; import { FarmAbiFactory } from 'src/modules/farm/farm.abi.factory'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; @Injectable() export class UserMetaEsdtComputeService { @@ -83,12 +81,11 @@ export class UserMetaEsdtComputeService { private readonly priceDiscoveryCompute: PriceDiscoveryComputeService, private readonly simpleLockService: SimpleLockService, private readonly lockedTokenWrapperService: LockedTokenWrapperService, - private readonly lockedTokenWrapperAbi: LockedTokenWrapperAbiService, private readonly energyAbi: EnergyAbiService, private readonly userEsdtCompute: UserEsdtComputeService, private readonly tokenGetter: TokenGetterService, private readonly cacheService: CachingService, - @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: Logger, ) {} async esdtTokenUSD(esdtToken: EsdtToken): Promise { @@ -211,9 +208,9 @@ export class UserMetaEsdtComputeService { ); } catch (e) { this.logger.error( - `Cannot compute farm token for nft ${JSON.stringify( - nftToken, - )}, error = ${e}`, + `Cannot compute farm token for nft ${JSON.stringify(nftToken)}`, + e.stack, + UserMetaEsdtComputeService.name, ); return undefined; } @@ -382,8 +379,11 @@ export class UserMetaEsdtComputeService { this.logger.error( `Cannot compute locked farm token for nft ${JSON.stringify( nftToken, - )}, error = ${e}`, + )}`, + e.stack, + UserMetaEsdtComputeService.name, ); + return undefined; } } diff --git a/src/modules/user/services/user.esdt.service.ts b/src/modules/user/services/user.esdt.service.ts index de57af828..a77639db9 100644 --- a/src/modules/user/services/user.esdt.service.ts +++ b/src/modules/user/services/user.esdt.service.ts @@ -14,6 +14,7 @@ import { UserToken } from '../models/user.model'; import { UserEsdtComputeService } from './esdt.compute.service'; import { PairAbiService } from 'src/modules/pair/services/pair.abi.service'; import { RouterAbiService } from 'src/modules/router/services/router.abi.service'; +import { GetOrSetCache } from 'src/helpers/decorators/caching.decorator'; @Injectable() export class UserEsdtService { @@ -27,13 +28,12 @@ export class UserEsdtService { private readonly cachingService: CachingService, ) {} - private async getUniquePairTokens(): Promise { - return await this.cachingService.getOrSet( - 'uniquePairTokens', - async () => await this.getUniquePairTokensRaw(), - oneSecond() * 6, - oneSecond() * 3, - ); + @GetOrSetCache({ + baseKey: 'user', + remoteTtl: oneSecond() * 6, + }) + private async uniquePairTokens(): Promise { + return await this.getUniquePairTokensRaw(); } private async getUniquePairTokensRaw(): Promise { @@ -66,7 +66,7 @@ export class UserEsdtService { pagination.offset, pagination.limit, ), - this.getUniquePairTokens(), + this.uniquePairTokens(), ]); const userPairEsdtTokens = userTokens.filter((token) => diff --git a/src/modules/user/services/user.metaEsdt.service.ts b/src/modules/user/services/user.metaEsdt.service.ts index bc0f7f50d..06570fed9 100644 --- a/src/modules/user/services/user.metaEsdt.service.ts +++ b/src/modules/user/services/user.metaEsdt.service.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { NftToken } from 'src/modules/tokens/models/nftToken.model'; -import { PairService } from 'src/modules/pair/services/pair.service'; import { MXApiService } from '../../../services/multiversx-communication/mx.api.service'; import { UserNftTokens } from '../models/nfttokens.union'; import { UserMetaEsdtComputeService } from './metaEsdt.compute.service'; @@ -13,12 +12,8 @@ import { LockedFarmToken, LockedFarmTokenV2, } from 'src/modules/tokens/models/lockedFarmToken.model'; -import { generateCacheKeyFromParams } from '../../../utils/generate-cache-key'; -import { CachingService } from '../../../services/caching/cache.service'; import { oneHour } from '../../../helpers/helpers'; -import { generateGetLogMessage } from '../../../utils/generate-log-message'; -import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; -import { Logger } from 'winston'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { PaginationArgs } from '../../dex.model'; import { LockedAssetGetterService } from '../../locked-asset-factory/services/locked.asset.getter.service'; import { farmsAddresses } from 'src/utils/farm.utils'; @@ -55,6 +50,8 @@ import { StakingAbiService } from 'src/modules/staking/services/staking.abi.serv import { SimpleLockAbiService } from 'src/modules/simple-lock/services/simple.lock.abi.service'; import { PriceDiscoveryAbiService } from 'src/modules/price-discovery/services/price.discovery.abi.service'; import { FarmAbiFactory } from 'src/modules/farm/farm.abi.factory'; +import { GetOrSetCache } from 'src/helpers/decorators/caching.decorator'; +import { ErrorLoggerAsync } from 'src/helpers/decorators/error.logger'; enum NftTokenType { FarmToken, LockedAssetToken, @@ -75,22 +72,21 @@ enum NftTokenType { @Injectable() export class UserMetaEsdtService { constructor( - private userComputeService: UserMetaEsdtComputeService, - private apiService: MXApiService, - private cachingService: CachingService, - private proxyPairAbi: ProxyPairAbiService, - private proxyFarmAbi: ProxyFarmAbiService, - private farmAbi: FarmAbiFactory, - private lockedAssetGetter: LockedAssetGetterService, - private stakingAbi: StakingAbiService, - private proxyStakeAbi: StakingProxyAbiService, - private priceDiscoveryService: PriceDiscoveryService, - private priceDiscoveryAbi: PriceDiscoveryAbiService, - private simpleLockAbi: SimpleLockAbiService, + private readonly userComputeService: UserMetaEsdtComputeService, + private readonly apiService: MXApiService, + private readonly proxyPairAbi: ProxyPairAbiService, + private readonly proxyFarmAbi: ProxyFarmAbiService, + private readonly farmAbi: FarmAbiFactory, + private readonly lockedAssetGetter: LockedAssetGetterService, + private readonly stakingAbi: StakingAbiService, + private readonly proxyStakeAbi: StakingProxyAbiService, + private readonly priceDiscoveryService: PriceDiscoveryService, + private readonly priceDiscoveryAbi: PriceDiscoveryAbiService, + private readonly simpleLockAbi: SimpleLockAbiService, private readonly energyAbi: EnergyAbiService, - private lockedTokenWrapperAbi: LockedTokenWrapperAbiService, + private readonly lockedTokenWrapperAbi: LockedTokenWrapperAbiService, private readonly remoteConfigGetterService: RemoteConfigGetterService, - @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: Logger, ) {} async getUserLockedAssetTokens( @@ -499,23 +495,9 @@ export class UserMetaEsdtService { const promises: Promise[] = []; for (const userNft of userNFTs) { - let userNftTokenType: NftTokenType; - try { - userNftTokenType = await this.cachingService.getOrSet( - `${userNft.collection}.metaEsdtType`, - () => this.getNftTokenType(userNft.collection), - oneHour(), - ); - } catch (error) { - const logMessage = generateGetLogMessage( - PairService.name, - this.getAllNftTokens.name, - `${userNft.collection}.metaEsdtType`, - error, - ); - this.logger.error(logMessage); - throw error; - } + const userNftTokenType = await this.nftTokenType( + userNft.collection, + ); switch (userNftTokenType) { case NftTokenType.FarmToken: @@ -623,7 +605,19 @@ export class UserMetaEsdtService { return await Promise.all(promises); } - private async getNftTokenType(tokenID: string): Promise { + @ErrorLoggerAsync({ + className: UserMetaEsdtService.name, + logArgs: true, + }) + @GetOrSetCache({ + baseKey: 'user', + remoteTtl: oneHour(), + }) + private async nftTokenType(tokenID: string): Promise { + return await this.getNftTokenTypeRaw(tokenID); + } + + private async getNftTokenTypeRaw(tokenID: string): Promise { const lockedMEXTokenID = await this.lockedAssetGetter.getLockedTokenID(); if (tokenID === lockedMEXTokenID) { @@ -732,8 +726,4 @@ export class UserMetaEsdtService { return undefined; } - - private getUserCacheKey(address: string, nonce: string, ...args: any) { - return generateCacheKeyFromParams('user', address, nonce, ...args); - } } diff --git a/src/modules/user/services/userEnergy/user.energy.compute.service.ts b/src/modules/user/services/userEnergy/user.energy.compute.service.ts index 6726fc01b..beb2246ae 100644 --- a/src/modules/user/services/userEnergy/user.energy.compute.service.ts +++ b/src/modules/user/services/userEnergy/user.energy.compute.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { scAddress } from '../../../../config'; import { EnergyType } from '@multiversx/sdk-exchange'; import { ClaimProgress } from '../../../../submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; @@ -15,14 +15,15 @@ import { StakingProxyService } from '../../../staking-proxy/services/staking.pro import { FarmVersion } from '../../../farm/models/farm.model'; import { farmVersion } from '../../../../utils/farm.utils'; import { BigNumber } from 'bignumber.js'; -import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; -import { Logger } from 'winston'; import { WeekTimekeepingAbiService } from 'src/submodules/week-timekeeping/services/week-timekeeping.abi.service'; import { WeeklyRewardsSplittingAbiService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; import { StakingProxyAbiService } from 'src/modules/staking-proxy/services/staking.proxy.abi.service'; import { FarmAbiFactory } from 'src/modules/farm/farm.abi.factory'; import { FarmFactoryService } from 'src/modules/farm/farm.factory'; import { FarmServiceV2 } from 'src/modules/farm/v2/services/farm.v2.service'; +import { GetOrSetCache } from 'src/helpers/decorators/caching.decorator'; +import { oneMinute } from 'src/helpers/helpers'; +import { EnergyAbiService } from 'src/modules/energy/services/energy.abi.service'; @Injectable() export class UserEnergyComputeService { @@ -34,50 +35,103 @@ export class UserEnergyComputeService { private readonly userMetaEsdtService: UserMetaEsdtService, private readonly stakeProxyService: StakingProxyService, private readonly stakeProxyAbi: StakingProxyAbiService, + private readonly energyAbi: EnergyAbiService, private readonly proxyService: ProxyService, - @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, ) {} + async getUserOutdatedContracts( + userAddress: string, + skipFeesCollector = false, + ): Promise { + const activeFarms = await this.userActiveFarmsV2(userAddress); + + const promises = activeFarms.map((farm) => + this.outdatedContract(userAddress, farm), + ); + if (!skipFeesCollector) { + promises.push( + this.outdatedContract(userAddress, scAddress.feesCollector), + ); + } + + const outdatedContracts = await Promise.all(promises); + return outdatedContracts.filter( + (contract) => contract && contract.address, + ); + } + + @GetOrSetCache({ + baseKey: 'userEnergy', + remoteTtl: oneMinute() * 10, + }) + async outdatedContract( + userAddress: string, + contractAddress: string, + ): Promise { + return await this.computeUserOutdatedContract( + userAddress, + contractAddress, + ); + } + async computeUserOutdatedContract( userAddress: string, - userEnergy: EnergyType, contractAddress: string, ): Promise { const isFarmAddress = contractAddress !== scAddress.feesCollector; if (isFarmAddress) { - const farmService = this.farmService.useService( + return await this.computeFarmOutdatedContract( + userAddress, contractAddress, - ) as FarmServiceV2; - const [currentClaimProgress, currentWeek, farmToken] = - await Promise.all([ - this.weeklyRewardsSplittingAbi.currentClaimProgress( - contractAddress, - userAddress, - ), - this.weekTimekeepingAbi.currentWeek(contractAddress), - farmService.getFarmToken(contractAddress), - ]); - - if (this.isEnergyOutdated(userEnergy, currentClaimProgress)) { - return new OutdatedContract({ - address: contractAddress, - type: ContractType.Farm, - claimProgressOutdated: - currentClaimProgress.week !== currentWeek, - farmToken: farmToken.collection, - }); - } - return new OutdatedContract(); + ); } - const [currentClaimProgress, currentWeek] = await Promise.all([ - this.weeklyRewardsSplittingAbi.currentClaimProgress( - contractAddress, - userAddress, - ), - this.weekTimekeepingAbi.currentWeek(contractAddress), - ]); + return await this.computeFeesCollectorOutdatedContract(userAddress); + } + + async computeFarmOutdatedContract( + userAddress: string, + contractAddress: string, + ): Promise { + const farmService = this.farmService.useService( + contractAddress, + ) as FarmServiceV2; + const [currentClaimProgress, currentWeek, farmToken, userEnergy] = + await Promise.all([ + this.weeklyRewardsSplittingAbi.currentClaimProgress( + contractAddress, + userAddress, + ), + this.weekTimekeepingAbi.currentWeek(contractAddress), + farmService.getFarmToken(contractAddress), + this.energyAbi.energyEntryForUser(userAddress), + ]); + + if (this.isEnergyOutdated(userEnergy, currentClaimProgress)) { + return new OutdatedContract({ + address: contractAddress, + type: ContractType.Farm, + claimProgressOutdated: + currentClaimProgress.week !== currentWeek, + farmToken: farmToken.collection, + }); + } + return new OutdatedContract(); + } + + async computeFeesCollectorOutdatedContract( + userAddress: string, + ): Promise { + const [currentClaimProgress, currentWeek, userEnergy] = + await Promise.all([ + this.weeklyRewardsSplittingAbi.currentClaimProgress( + scAddress.feesCollector, + userAddress, + ), + this.weekTimekeepingAbi.currentWeek(scAddress.feesCollector), + this.energyAbi.energyEntryForUser(userAddress), + ]); if (this.isEnergyOutdated(userEnergy, currentClaimProgress)) { return new OutdatedContract({ @@ -89,6 +143,14 @@ export class UserEnergyComputeService { return new OutdatedContract(); } + @GetOrSetCache({ + baseKey: 'userEnergy', + remoteTtl: oneMinute(), + }) + async userActiveFarmsV2(userAddress: string): Promise { + return await this.computeActiveFarmsV2ForUser(userAddress); + } + async computeActiveFarmsV2ForUser(userAddress: string): Promise { const maxPagination = new PaginationArgs({ limit: 100, diff --git a/src/modules/user/services/userEnergy/user.energy.getter.service.ts b/src/modules/user/services/userEnergy/user.energy.getter.service.ts deleted file mode 100644 index 1bd52e4dd..000000000 --- a/src/modules/user/services/userEnergy/user.energy.getter.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { GenericGetterService } from '../../../../services/generics/generic.getter.service'; -import { UserEnergyComputeService } from './user.energy.compute.service'; -import { CachingService } from '../../../../services/caching/cache.service'; -import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; -import { Logger } from 'winston'; -import { OutdatedContract } from '../../models/user.model'; -import { oneMinute } from '../../../../helpers/helpers'; -import { EnergyType } from '@multiversx/sdk-exchange'; -import { scAddress } from 'src/config'; -import { EnergyAbiService } from 'src/modules/energy/services/energy.abi.service'; - -@Injectable() -export class UserEnergyGetterService extends GenericGetterService { - constructor( - protected readonly cachingService: CachingService, - @Inject(WINSTON_MODULE_PROVIDER) protected readonly logger: Logger, - private readonly userEnergyCompute: UserEnergyComputeService, - private readonly energyAbi: EnergyAbiService, - ) { - super(cachingService, logger); - this.baseKey = 'userEnergy'; - } - - async getUserOutdatedContract( - userAddress: string, - userEnergy: EnergyType, - contractAddress: string, - ): Promise { - return await this.getData( - this.getCacheKey('outdatedContract', userAddress, contractAddress), - () => - this.userEnergyCompute.computeUserOutdatedContract( - userAddress, - userEnergy, - contractAddress, - ), - oneMinute() * 10, - ); - } - - async getUserOutdatedContracts( - userAddress: string, - skipFeesCollector = false, - ): Promise { - const activeFarms = await this.getUserActiveFarmsV2(userAddress); - const userEnergy = await this.energyAbi.energyEntryForUser(userAddress); - - const promises = activeFarms.map((farm) => - this.getUserOutdatedContract(userAddress, userEnergy, farm), - ); - if (!skipFeesCollector) { - promises.push( - this.getUserOutdatedContract( - userAddress, - userEnergy, - scAddress.feesCollector, - ), - ); - } - - const outdatedContracts = await Promise.all(promises); - return outdatedContracts.filter( - (contract) => contract && contract.address, - ); - } - - async getUserActiveFarmsV2(userAddress: string): Promise { - return this.getData( - this.getCacheKey('userActiveFarms', userAddress), - () => - this.userEnergyCompute.computeActiveFarmsV2ForUser(userAddress), - oneMinute(), - ); - } -} diff --git a/src/modules/user/services/userEnergy/user.energy.transaction.service.ts b/src/modules/user/services/userEnergy/user.energy.transaction.service.ts index 48df333d0..6e247a2d3 100644 --- a/src/modules/user/services/userEnergy/user.energy.transaction.service.ts +++ b/src/modules/user/services/userEnergy/user.energy.transaction.service.ts @@ -3,13 +3,13 @@ import { Injectable } from '@nestjs/common'; import { gasConfig, mxConfig, scAddress } from 'src/config'; import { TransactionModel } from 'src/models/transaction.model'; import { MXProxyService } from 'src/services/multiversx-communication/mx.proxy.service'; -import { UserEnergyGetterService } from './user.energy.getter.service'; +import { UserEnergyComputeService } from './user.energy.compute.service'; @Injectable() export class UserEnergyTransactionService { constructor( private readonly mxProxy: MXProxyService, - private readonly userEnergyGetter: UserEnergyGetterService, + private readonly userEnergyCompute: UserEnergyComputeService, ) {} async updateFarmsEnergyForUser( @@ -22,7 +22,7 @@ export class UserEnergyTransactionService { ]; if (includeAllContracts) { - const farms = await this.userEnergyGetter.getUserActiveFarmsV2( + const farms = await this.userEnergyCompute.userActiveFarmsV2( userAddress, ); farms.forEach((farm) => { @@ -37,7 +37,7 @@ export class UserEnergyTransactionService { } } else { const contracts = - await this.userEnergyGetter.getUserOutdatedContracts( + await this.userEnergyCompute.getUserOutdatedContracts( userAddress, skipFeesCollector, ); diff --git a/src/modules/user/specs/user.service.spec.ts b/src/modules/user/specs/user.service.spec.ts index 42ad81958..e0f65da1d 100644 --- a/src/modules/user/specs/user.service.spec.ts +++ b/src/modules/user/specs/user.service.spec.ts @@ -2,33 +2,20 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PairService } from '../../pair/services/pair.service'; import { ProxyService } from '../../proxy/services/proxy.service'; import { UserMetaEsdtService } from '../services/user.metaEsdt.service'; -import { MXApiService } from '../../../services/multiversx-communication/mx.api.service'; import { LockedAssetService } from '../../locked-asset-factory/services/locked-asset.service'; -import { - utilities as nestWinstonModuleUtilities, - WinstonModule, -} from 'nest-winston'; -import * as winston from 'winston'; -import * as Transport from 'winston-transport'; -import { MXApiServiceMock } from '../../../services/multiversx-communication/mx.api.service.mock'; +import { MXApiServiceProvider } from '../../../services/multiversx-communication/mx.api.service.mock'; import { UserFarmToken, UserToken } from '../models/user.model'; import { FarmTokenAttributesModelV1_2 } from '../../farm/models/farmTokenAttributes.model'; import { UserMetaEsdtComputeService } from '../services/metaEsdt.compute.service'; import { CachingModule } from '../../../services/caching/cache.module'; -import { LockedAssetServiceMock } from '../../locked-asset-factory/mocks/locked.asset.service.mock'; import { LockedAssetGetterService } from '../../locked-asset-factory/services/locked.asset.getter.service'; -import { AbiLockedAssetService } from '../../locked-asset-factory/services/abi-locked-asset.service'; -import { AbiLockedAssetServiceMock } from '../../locked-asset-factory/mocks/abi.locked.asset.service.mock'; -import { ContextGetterService } from 'src/services/context/context.getter.service'; -import { ContextGetterServiceMock } from 'src/services/context/mocks/context.getter.service.mock'; -import { StakingService } from '../../staking/services/staking.service'; -import { StakingServiceMock } from '../../staking/mocks/staking.service.mock'; -import { StakingProxyService } from '../../staking-proxy/services/staking.proxy.service'; -import { StakingProxyServiceMock } from '../../staking-proxy/mocks/staking.proxy.service.mock'; +import { AbiLockedAssetServiceProvider } from '../../locked-asset-factory/mocks/abi.locked.asset.service.mock'; +import { ContextGetterServiceProvider } from 'src/services/context/mocks/context.getter.service.mock'; +import { StakingServiceProvider } from '../../staking/mocks/staking.service.mock'; +import { StakingProxyServiceProvider } from '../../staking-proxy/mocks/staking.proxy.service.mock'; import { PriceDiscoveryServiceProvider } from '../../price-discovery/mocks/price.discovery.service.mock'; import { SimpleLockService } from '../../simple-lock/services/simple.lock.service'; -import { RemoteConfigGetterService } from '../../remote-config/remote-config.getter.service'; -import { RemoteConfigGetterServiceMock } from '../../remote-config/mocks/remote-config.getter.mock'; +import { RemoteConfigGetterServiceProvider } from '../../remote-config/mocks/remote-config.getter.mock'; import { TokenGetterServiceProvider } from '../../tokens/mocks/token.getter.service.mock'; import { UserEsdtService } from '../services/user.esdt.service'; import { TokenService } from 'src/modules/tokens/services/token.service'; @@ -75,58 +62,14 @@ import { FarmAbiServiceProviderV1_3 } from 'src/modules/farm/mocks/farm.v1.3.abi import { WeeklyRewardsSplittingComputeService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.compute.service'; import { FarmAbiFactory } from 'src/modules/farm/farm.abi.factory'; import { FarmServiceBaseMock } from 'src/modules/farm/mocks/farm.service.mock'; +import { CommonAppModule } from 'src/common.app.module'; import { Address } from '@multiversx/sdk-core/out'; describe('UserService', () => { - let userMetaEsdts: UserMetaEsdtService; - let userEsdts: UserEsdtService; - - const MXApiServiceProvider = { - provide: MXApiService, - useClass: MXApiServiceMock, - }; - - const ContextGetterServiceProvider = { - provide: ContextGetterService, - useClass: ContextGetterServiceMock, - }; - - const LockedAssetProvider = { - provide: LockedAssetService, - useClass: LockedAssetServiceMock, - }; - - const AbiLockedAssetServiceProvider = { - provide: AbiLockedAssetService, - useClass: AbiLockedAssetServiceMock, - }; - - const StakingServiceProvider = { - provide: StakingService, - useClass: StakingServiceMock, - }; - - const StakingProxyServiceProvider = { - provide: StakingProxyService, - useClass: StakingProxyServiceMock, - }; - - const RemoteConfigGetterServiceProvider = { - provide: RemoteConfigGetterService, - useClass: RemoteConfigGetterServiceMock, - }; - - const logTransports: Transport[] = [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.timestamp(), - nestWinstonModuleUtilities.format.nestLike(), - ), - }), - ]; + let module: TestingModule; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ + module = await Test.createTestingModule({ providers: [ MXApiServiceProvider, ContextGetterServiceProvider, @@ -167,7 +110,7 @@ describe('UserService', () => { ProxyFarmAbiServiceProvider, UserMetaEsdtComputeService, LockedTokenWrapperService, - LockedAssetProvider, + LockedAssetService, AbiLockedAssetServiceProvider, LockedAssetGetterService, WrapAbiServiceProvider, @@ -192,24 +135,22 @@ describe('UserService', () => { RemoteConfigGetterServiceProvider, MXDataApiServiceProvider, ], - imports: [ - WinstonModule.forRoot({ - transports: logTransports, - }), - CachingModule, - ], + imports: [CommonAppModule, CachingModule], }).compile(); - - userEsdts = module.get(UserEsdtService); - userMetaEsdts = module.get(UserMetaEsdtService); }); it('should be defined', () => { + const userEsdts = module.get(UserEsdtService); + const userMetaEsdts = + module.get(UserMetaEsdtService); + expect(userEsdts).toBeDefined(); expect(userMetaEsdts).toBeDefined(); }); it('should get user esdt tokens', async () => { + const userEsdts = module.get(UserEsdtService); + expect( await userEsdts.getAllEsdtTokens( Address.fromHex( @@ -255,6 +196,9 @@ describe('UserService', () => { }); it('should get user nfts tokens', async () => { + const userMetaEsdts = + module.get(UserMetaEsdtService); + expect( await userMetaEsdts.getAllNftTokens( Address.fromHex( diff --git a/src/modules/user/user.info-by-week.resolver.ts b/src/modules/user/user.info-by-week.resolver.ts index fd2623615..532683879 100644 --- a/src/modules/user/user.info-by-week.resolver.ts +++ b/src/modules/user/user.info-by-week.resolver.ts @@ -3,7 +3,6 @@ import { TokenDistributionModel, UserInfoByWeekModel, } from '../../submodules/weekly-rewards-splitting/models/weekly-rewards-splitting.model'; -import { GenericResolver } from '../../services/generics/generic.resolver'; import { EnergyModel } from '../energy/models/energy.model'; import { EsdtTokenPayment } from '../../models/esdtTokenPayment.model'; import { scAddress } from '../../config'; @@ -13,37 +12,31 @@ import { FeesCollectorComputeService } from '../fees-collector/services/fees-col import { FarmComputeServiceV2 } from '../farm/v2/services/farm.v2.compute.service'; @Resolver(() => UserInfoByWeekModel) -export class UserInfoByWeekResolver extends GenericResolver { +export class UserInfoByWeekResolver { constructor( private readonly farmComputeV2: FarmComputeServiceV2, private readonly feesCollectorCompute: FeesCollectorComputeService, private readonly weeklyRewardsSplittingAbi: WeeklyRewardsSplittingAbiService, private readonly weeklyRewardsSplittingCompute: WeeklyRewardsSplittingComputeService, - ) { - super(); - } + ) {} @ResolveField(() => EnergyModel) async energyForWeek( @Parent() parent: UserInfoByWeekModel, ): Promise { - return await this.genericFieldResolver(() => - this.weeklyRewardsSplittingAbi.userEnergyForWeek( - parent.scAddress, - parent.userAddress, - parent.week, - ), + return this.weeklyRewardsSplittingAbi.userEnergyForWeek( + parent.scAddress, + parent.userAddress, + parent.week, ); } @ResolveField() async apr(@Parent() parent: UserInfoByWeekModel): Promise { - return await this.genericFieldResolver(() => - this.weeklyRewardsSplittingCompute.userApr( - parent.scAddress, - parent.userAddress, - parent.week, - ), + return this.weeklyRewardsSplittingCompute.userApr( + parent.scAddress, + parent.userAddress, + parent.week, ); } diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 43381ea98..bf4c2ba10 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -25,7 +25,6 @@ import { FarmModule } from '../farm/farm.module'; import { EnergyModule } from '../energy/energy.module'; import { UserNftsResolver } from './user.nfts.resolver'; import { FeesCollectorModule } from '../fees-collector/fees-collector.module'; -import { UserEnergyGetterService } from './services/userEnergy/user.energy.getter.service'; import { UserEnergyComputeService } from './services/userEnergy/user.energy.compute.service'; import { LockedTokenWrapperModule } from '../locked-token-wrapper/locked-token-wrapper.module'; import { UserEnergySetterService } from './services/userEnergy/user.energy.setter.service'; @@ -67,7 +66,6 @@ import { FarmModuleV2 } from '../farm/v2/farm.v2.module'; UserEsdtComputeService, UserMetaEsdtComputeService, UserEnergyComputeService, - UserEnergyGetterService, UserEnergySetterService, UserEnergyTransactionService, UserResolver, @@ -79,7 +77,6 @@ import { FarmModuleV2 } from '../farm/v2/farm.v2.module'; UserMetaEsdtService, UserInfoByWeekResolver, UserEnergyComputeService, - UserEnergyGetterService, UserEnergySetterService, UserEnergyTransactionService, ], diff --git a/src/modules/user/user.nfts.resolver.ts b/src/modules/user/user.nfts.resolver.ts index fa998b69c..e76f45a09 100644 --- a/src/modules/user/user.nfts.resolver.ts +++ b/src/modules/user/user.nfts.resolver.ts @@ -2,7 +2,6 @@ import { UseGuards } from '@nestjs/common'; import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthUser } from '../auth/auth.user'; import { UserAuthResult } from '../auth/user.auth.result'; -import { GenericResolver } from 'src/services/generics/generic.resolver'; import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth.guard'; import { PaginationArgs } from '../dex.model'; import { @@ -26,20 +25,16 @@ import { import { UserMetaEsdtService } from './services/user.metaEsdt.service'; @Resolver(() => UserNftsModel) -export class UserNftsResolver extends GenericResolver { - constructor(private readonly userMetaEsdts: UserMetaEsdtService) { - super(); - } +export class UserNftsResolver { + constructor(private readonly userMetaEsdts: UserMetaEsdtService) {} @ResolveField() async userLockedAssetToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedAssetTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedAssetTokens( + parent.address, + parent.pagination, ); } @@ -47,11 +42,9 @@ export class UserNftsResolver extends GenericResolver { async userFarmToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserFarmTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserFarmTokens( + parent.address, + parent.pagination, ); } @@ -59,11 +52,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedLPToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedLpTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedLpTokens( + parent.address, + parent.pagination, ); } @@ -71,11 +62,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedFarmToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedFarmTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedFarmTokens( + parent.address, + parent.pagination, ); } @@ -83,11 +72,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedLpTokenV2( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedLpTokensV2( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedLpTokensV2( + parent.address, + parent.pagination, ); } @@ -95,11 +82,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedFarmTokenV2( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedFarmTokensV2( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedFarmTokensV2( + parent.address, + parent.pagination, ); } @@ -107,11 +92,9 @@ export class UserNftsResolver extends GenericResolver { async userStakeFarmToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserStakeFarmTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserStakeFarmTokens( + parent.address, + parent.pagination, ); } @@ -119,11 +102,9 @@ export class UserNftsResolver extends GenericResolver { async userUnbondFarmToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserUnbondFarmTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserUnbondFarmTokens( + parent.address, + parent.pagination, ); } @@ -131,11 +112,9 @@ export class UserNftsResolver extends GenericResolver { async userDualYieldToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserDualYieldTokens( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserDualYieldTokens( + parent.address, + parent.pagination, ); } @@ -143,11 +122,9 @@ export class UserNftsResolver extends GenericResolver { async userRedeemToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserRedeemToken( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserRedeemToken( + parent.address, + parent.pagination, ); } @@ -155,11 +132,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedEsdtToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedEsdtToken( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedEsdtToken( + parent.address, + parent.pagination, ); } @@ -167,11 +142,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedSimpleLpToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedSimpleLpToken( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedSimpleLpToken( + parent.address, + parent.pagination, ); } @@ -179,11 +152,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedSimpleFarmToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedSimpleFarmToken( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedSimpleFarmToken( + parent.address, + parent.pagination, ); } @@ -191,11 +162,9 @@ export class UserNftsResolver extends GenericResolver { async userLockedTokenEnergy( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserLockedTokenEnergy( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserLockedTokenEnergy( + parent.address, + parent.pagination, ); } @@ -203,11 +172,9 @@ export class UserNftsResolver extends GenericResolver { async userWrappedLockedToken( @Parent() parent: UserNftsModel, ): Promise { - return await this.genericFieldResolver(() => - this.userMetaEsdts.getUserWrappedLockedTokenEnergy( - parent.address, - parent.pagination, - ), + return this.userMetaEsdts.getUserWrappedLockedTokenEnergy( + parent.address, + parent.pagination, ); } diff --git a/src/modules/user/user.resolver.ts b/src/modules/user/user.resolver.ts index 910f8d79d..75f404a4a 100644 --- a/src/modules/user/user.resolver.ts +++ b/src/modules/user/user.resolver.ts @@ -8,20 +8,19 @@ import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth.guard'; import { AuthUser } from '../auth/auth.user'; import { UserAuthResult } from '../auth/user.auth.result'; import { EsdtTokenInput } from '../tokens/models/esdtTokenInput.model'; -import { ApolloError } from 'apollo-server-express'; import { Address } from '@multiversx/sdk-core'; import { NftTokenInput } from '../tokens/models/nftTokenInput.model'; import { UserEsdtService } from './services/user.esdt.service'; import { TransactionModel } from '../../models/transaction.model'; import { UserEnergyTransactionService } from './services/userEnergy/user.energy.transaction.service'; -import { UserEnergyGetterService } from './services/userEnergy/user.energy.getter.service'; +import { UserEnergyComputeService } from './services/userEnergy/user.energy.compute.service'; @Resolver() export class UserResolver { constructor( private readonly userEsdt: UserEsdtService, private readonly userMetaEsdt: UserMetaEsdtService, - private readonly userEnergyGetter: UserEnergyGetterService, + private readonly userEnergyCompute: UserEnergyComputeService, private readonly userEnergyTransaction: UserEnergyTransactionService, ) {} @@ -31,7 +30,7 @@ export class UserResolver { @AuthUser() user: UserAuthResult, @Args() pagination: PaginationArgs, ): Promise { - return await this.userEsdt.getAllEsdtTokens(user.address, pagination); + return this.userEsdt.getAllEsdtTokens(user.address, pagination); } @UseGuards(JwtOrNativeAuthGuard) @@ -54,7 +53,7 @@ export class UserResolver { skipFeesCollector: boolean, @AuthUser() user: UserAuthResult, ): Promise { - return await this.userEnergyGetter.getUserOutdatedContracts( + return this.userEnergyCompute.getUserOutdatedContracts( user.address, skipFeesCollector, ); @@ -69,7 +68,7 @@ export class UserResolver { @Args('skipFeesCollector', { nullable: true }) skipFeesCollector: boolean, ): Promise { - return await this.userEnergyTransaction.updateFarmsEnergyForUser( + return this.userEnergyTransaction.updateFarmsEnergyForUser( user.address, includeAllContracts, skipFeesCollector, @@ -82,15 +81,11 @@ export class UserResolver { @Args('tokens', { type: () => [EsdtTokenInput] }) tokens: EsdtTokenInput[], ): Promise { - try { - return await this.userEsdt.getAllEsdtTokens( - Address.Zero().bech32(), - pagination, - tokens, - ); - } catch (error) { - throw new ApolloError(error); - } + return this.userEsdt.getAllEsdtTokens( + Address.Zero().bech32(), + pagination, + tokens, + ); } @Query(() => [UserNftTokens]) @@ -98,14 +93,10 @@ export class UserResolver { @Args() pagination: PaginationArgs, @Args('nfts', { type: () => [NftTokenInput] }) nfts: NftTokenInput[], ): Promise { - try { - return await this.userMetaEsdt.getAllNftTokens( - Address.Zero().bech32(), - pagination, - nfts, - ); - } catch (error) { - throw new ApolloError(error); - } + return this.userMetaEsdt.getAllNftTokens( + Address.Zero().bech32(), + pagination, + nfts, + ); } } diff --git a/src/public.app.module.ts b/src/public.app.module.ts index c349f7649..bd955e241 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -1,10 +1,4 @@ -import { - CacheModule, - LoggerService, - MiddlewareConsumer, - Module, - RequestMethod, -} from '@nestjs/common'; +import { CacheModule, LoggerService, MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { RouterModule } from './modules/router/router.module'; import { PairModule } from './modules/pair/pair.module'; @@ -36,6 +30,7 @@ import { TokenUnstakeModule } from './modules/token-unstake/token.unstake.module import { LockedTokenWrapperModule } from './modules/locked-token-wrapper/locked-token-wrapper.module'; import { GuestCachingMiddleware } from './utils/guestCaching.middleware'; import { EscrowModule } from './modules/escrow/escrow.module'; +import { GovernanceModule } from './modules/governance/governance.module'; @Module({ imports: [ @@ -112,6 +107,7 @@ import { EscrowModule } from './modules/escrow/escrow.module'; TokenUnstakeModule, LockedTokenWrapperModule, EscrowModule, + GovernanceModule, ], providers: [CachingService], }) diff --git a/src/services/analytics/interfaces/analytics.query.interface.ts b/src/services/analytics/interfaces/analytics.query.interface.ts index 3f5674e59..71898980a 100644 --- a/src/services/analytics/interfaces/analytics.query.interface.ts +++ b/src/services/analytics/interfaces/analytics.query.interface.ts @@ -25,5 +25,7 @@ export interface AnalyticsQueryInterface { series, metric, timeBucket, + startDate, + endDate, }): Promise; } diff --git a/src/services/analytics/services/analytics.query.service.ts b/src/services/analytics/services/analytics.query.service.ts index e45d2b333..82b178644 100644 --- a/src/services/analytics/services/analytics.query.service.ts +++ b/src/services/analytics/services/analytics.query.service.ts @@ -61,12 +61,16 @@ export class AnalyticsQueryService implements AnalyticsQueryInterface { series, metric, timeBucket, + startDate, + endDate, }): Promise { const service = await this.getService(); return await service.getPDCloseValues({ series, metric, timeBucket, + startDate, + endDate, }); } diff --git a/src/services/analytics/timescaledb/entities/timescaledb.entities.ts b/src/services/analytics/timescaledb/entities/timescaledb.entities.ts index 5d7e7c62d..73fae4d02 100644 --- a/src/services/analytics/timescaledb/entities/timescaledb.entities.ts +++ b/src/services/analytics/timescaledb/entities/timescaledb.entities.ts @@ -197,7 +197,6 @@ export class CloseHourly { 'acceptedTokenPrice', 'launchedTokenPriceUSD', 'acceptedTokenPriceUSD') - AND timestamp >= NOW() - INTERVAL '1 day' GROUP BY time, series, key; `, materialized: true, diff --git a/src/services/analytics/timescaledb/migration/1684167998347-xExchange.ts b/src/services/analytics/timescaledb/migration/1684167998347-xExchange.ts index fe73e653d..31ea3c680 100644 --- a/src/services/analytics/timescaledb/migration/1684167998347-xExchange.ts +++ b/src/services/analytics/timescaledb/migration/1684167998347-xExchange.ts @@ -18,7 +18,6 @@ export class xExchange1684167998347 implements MigrationInterface { 'acceptedTokenPrice', 'launchedTokenPriceUSD', 'acceptedTokenPriceUSD') - AND timestamp >= NOW() - INTERVAL '1 day' GROUP BY time, series, key; `); await queryRunner.query( diff --git a/src/services/analytics/timescaledb/timescaledb.query.service.ts b/src/services/analytics/timescaledb/timescaledb.query.service.ts index a84c638b2..485643c5b 100644 --- a/src/services/analytics/timescaledb/timescaledb.query.service.ts +++ b/src/services/analytics/timescaledb/timescaledb.query.service.ts @@ -308,6 +308,8 @@ export class TimescaleDBQueryService implements AnalyticsQueryInterface { series, metric, timeBucket, + startDate, + endDate, }): Promise { const query = await this.pdCloseMinute .createQueryBuilder() @@ -315,7 +317,10 @@ export class TimescaleDBQueryService implements AnalyticsQueryInterface { .addSelect('locf(last(last, time)) as last') .where('series = :series', { series }) .andWhere('key = :metric', { metric }) - .andWhere("time between now() - INTERVAL '1 day' and now()") + .andWhere('time between :start and :end', { + start: startDate, + end: endDate, + }) .groupBy('bucket') .getRawMany(); diff --git a/src/services/multiversx-communication/mx.api.service.ts b/src/services/multiversx-communication/mx.api.service.ts index 5d4ca7c87..1860e927b 100644 --- a/src/services/multiversx-communication/mx.api.service.ts +++ b/src/services/multiversx-communication/mx.api.service.ts @@ -304,6 +304,15 @@ export class MXApiService { return latestBlock[0].nonce; } + async getBlockByNonce(shardId: number, nonce: number): Promise { + const blocks = await this.doGetGeneric( + this.getBlockByNonce.name, + `blocks?nonce=${nonce}&shard=${shardId}`, + ); + + return blocks[0] ?? undefined; + } + async getShardTimestamp(shardId: number): Promise { const latestShardBlock = await this.doGetGeneric( this.getShardTimestamp.name, diff --git a/src/services/multiversx-communication/mx.proxy.service.ts b/src/services/multiversx-communication/mx.proxy.service.ts index 1c227913b..480068963 100644 --- a/src/services/multiversx-communication/mx.proxy.service.ts +++ b/src/services/multiversx-communication/mx.proxy.service.ts @@ -1,9 +1,4 @@ -import { - AbiRegistry, - Address, - SmartContract, - SmartContractAbi, -} from '@multiversx/sdk-core'; +import { AbiRegistry, Address, SmartContract, SmartContractAbi } from '@multiversx/sdk-core'; import { Inject, Injectable } from '@nestjs/common'; import { abiConfig, mxConfig, scAddress } from '../../config'; import Agent, { HttpsAgent } from 'agentkeepalive'; @@ -216,6 +211,16 @@ export class MXProxyService { ); } + async getGovernanceSmartContract( + governanceAddress: string, + ): Promise { + return this.getSmartContract( + governanceAddress, + abiConfig.governance, + 'GovernanceV2', + ); + } + async getSmartContract( contractAddress: string, contractAbiPath: string, diff --git a/src/utils/governance.ts b/src/utils/governance.ts new file mode 100644 index 000000000..d837c6db2 --- /dev/null +++ b/src/utils/governance.ts @@ -0,0 +1,62 @@ +import { GovernanceType } from '../modules/governance/models/governance.contract.model'; +import { scAddress } from '../config'; +import { GovernanceProposalStatus } from '../modules/governance/models/governance.proposal.model'; + +const toTypeEnum = (type: string): GovernanceType => { + switch (type) { + case 'energy': + return GovernanceType.ENERGY; + case 'token': + return GovernanceType.TOKEN; + default: + return undefined; + } +}; + + +export const governanceType = (governanceAddress: string): GovernanceType | undefined => { + const govConfig = scAddress.governance + const types = Object.keys(scAddress.governance); + for (const type of types) { + const address = govConfig[type].find( + (address: string) => address === governanceAddress, + ); + if (address !== undefined) { + return toTypeEnum(type); + } + } + return undefined; +}; + + +export const governanceContractsAddresses = (types?: string[]): string[] => { + const govConfig = scAddress.governance + const addresses = []; + if (types === undefined || types.length === 0) { + types = Object.keys(govConfig) + } + for (const type of types) { + addresses.push(...govConfig[type]); + } + return addresses; +}; + + +export const toGovernanceProposalStatus = (status: string): GovernanceProposalStatus => { + switch (status) { + case 'None': + return GovernanceProposalStatus.None; + case 'Pending': + return GovernanceProposalStatus.Pending; + case 'Active': + return GovernanceProposalStatus.Active; + case 'Defeated': + return GovernanceProposalStatus.Defeated; + case 'DefeatedWithVeto': + return GovernanceProposalStatus.DefeatedWithVeto; + case 'Succeeded': + return GovernanceProposalStatus.Succeeded; + default: + return undefined; + } +};