Skip to content

Commit

Permalink
feat: add wallet rewards history
Browse files Browse the repository at this point in the history
  • Loading branch information
yevhen-burkovskyi committed Jul 8, 2024
1 parent cf98e2e commit be482b8
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 17 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ PROPOSAL_CACHE_TTL=120 #2 mins
SUPPLY_CHANGE_CACHE_TTL=120 #2 mins
PROPOSAL_VOTERS_TTL=120 #2 mins
VALIDATOR_RECENTLY_PROPOSED_BLOCK_TTL=600 #10 min
WALLET_REWARD_HISTORY_TTL=60 #1 min

# keybase
KEYBASE_URL=https://keybase.io/_/api/1.0
Expand Down
1 change: 1 addition & 0 deletions src/core/config/config.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface CacheConfig {
supplyChange: number;
proposalVoters: number;
validatorRecentlyProposedBlock: number;
walletRewardHistory: number;
}

export interface KeybaseConfig {
Expand Down
1 change: 1 addition & 0 deletions src/core/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export const ConfigSchema = Joi.object({
EXCHANGE_RATE_API_KEY: Joi.string().required(),
PROPOSAL_VOTERS_TTL: Joi.string().required(),
VALIDATOR_RECENTLY_PROPOSED_BLOCK_TTL: Joi.string().required(),
WALLET_REWARD_HISTORY_TTL: Joi.string().required(),
}).required();
1 change: 1 addition & 0 deletions src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const config: ConfigDto = {
supplyChange: +process.env.SUPPLY_CHANGE_CACHE_TTL!,
proposalVoters: +process.env.PROPOSAL_VOTERS_TTL!,
validatorRecentlyProposedBlock: +process.env.VALIDATOR_RECENTLY_PROPOSED_BLOCK_TTL!,
walletRewardHistory: +process.env.WALLET_REWARD_HISTORY_TTL!,
},
keybase: {
url: process.env.KEYBASE_URL!,
Expand Down
3 changes: 2 additions & 1 deletion src/core/lib/okp4/enums/endpoints.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export enum Endpoints {
INFLATION = 'cosmos/mint/v1beta1/inflation',
DISTRIBUTION_PARAMS = 'cosmos/distribution/v1beta1/params',
BALANCES = 'cosmos/bank/v1beta1/balances/:address',
PROPOSAL_VOTES = '/cosmos/gov/v1/proposals/:proposal_id/votes',
PROPOSAL_VOTES = 'cosmos/gov/v1/proposals/:proposal_id/votes',
TXS = 'cosmos/tx/v1beta1/txs',
}
26 changes: 26 additions & 0 deletions src/core/lib/okp4/okp4.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { DistributionParamsResponse } from "./responses/distribution-params.resp
import Big from "big.js";
import { BalancesResponse } from "./responses/balances.response";
import { GetProposalVotesResponse } from "./responses/get-proposal-votes.response";
import { RewardsHistoryResponse } from "./responses/rewards-history.response";

@Injectable()
export class Okp4Service {
Expand Down Expand Up @@ -308,6 +309,31 @@ export class Okp4Service {
);
}

async getWalletRewardsHistory(address: string, limit?: number, offset?: number): Promise<RewardsHistoryResponse> {
const wallet = {
"query": `message.sender='${address}'`,
};
let pagination = undefined;

if(limit !== undefined && offset !== undefined) {
pagination = {
"pagination.offset": offset.toString(),
"pagination.limit": limit.toString(),
"pagination.count_total": true.toString(),
}
}

return this.getWithErrorHandling(
this.constructUrl(
Endpoints.TXS,
createUrlParams({
...pagination,
...wallet,
})
)
);
}

private okp4Pagination(limit: number, offset: number) {
return createUrlParams({
"pagination.offset": offset.toString(),
Expand Down
36 changes: 36 additions & 0 deletions src/core/lib/okp4/responses/rewards-history.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { WithPaginationResponse } from "./with-pagination.response";

export type RewardsHistoryResponse = WithPaginationResponse<{
tx_responses: Tx[];
}>;


export interface Tx {
txhash: string;
code: number;
timestamp: string;
tx: {
body: {
messages: Array<{
"@type": string
}>;
}
};
events: Event[];
}

export interface Event {
type: string;
attributes: [
{
key: string;
value: string;
index: string;
},
{
key: string;
value: string;
index: string;
}
]
}
16 changes: 5 additions & 11 deletions src/modules/governance/services/governance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { ProposalStatusEnum } from "@core/lib/okp4/enums/proposal-status.enum";
import { GovernanceCache } from "./governance.cache";
import { Log } from "@core/loggers/log";
import { toPercents } from "@utils/to-percents";
import { createHash } from "crypto";
import { GetProposalVotesDto } from "../dto/get-proposal-votes.dto";
import Big from "big.js";
import { Pagination } from "@core/types/pagination.dto";
import { hash } from "@utils/create-hash";

@Injectable()
export class GovernanceService implements OnModuleInit {
Expand Down Expand Up @@ -68,7 +68,7 @@ export class GovernanceService implements OnModuleInit {
}

async getProposals(payload: Pagination) {
const cache = await this.cache.getProposals(this.createParamHash(payload));
const cache = await this.cache.getProposals(hash(payload));

if (cache === null) {
return this.fetchProposals(payload);
Expand Down Expand Up @@ -98,7 +98,7 @@ export class GovernanceService implements OnModuleInit {
proposals: proposalsWithTurnout,
};

await this.cache.setProposals(view, this.createParamHash({ limit, offset }));
await this.cache.setProposals(view, hash({ limit, offset }));

return view;
}
Expand Down Expand Up @@ -201,9 +201,7 @@ export class GovernanceService implements OnModuleInit {
}

async getProposalVotes(payload: GetProposalVotesDto) {
const cache = await this.cache.getProposalVotes(
this.createParamHash(payload)
);
const cache = await this.cache.getProposalVotes(hash(payload));

if (!cache) {
return this.fetchProposalVotes(payload);
Expand All @@ -230,7 +228,7 @@ export class GovernanceService implements OnModuleInit {
option: maxWeightOption.option,
};
});
await this.cache.setProposalVotes(this.createParamHash(payload), voters);
await this.cache.setProposalVotes(hash(payload), voters);
return {
voters,
pagination: {
Expand All @@ -240,8 +238,4 @@ export class GovernanceService implements OnModuleInit {
},
};
}

private createParamHash(params: unknown): string {
return createHash("sha256").update(JSON.stringify(params)).digest("hex");
}
}
File renamed without changes.
6 changes: 6 additions & 0 deletions src/modules/wallet/dtos/get-wallet-rewards-history.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface GetWalletRewardsHistoryDto {
address: string;
limit?: number;
offset?: number;
}

4 changes: 4 additions & 0 deletions src/modules/wallet/enums/wallet-prefix.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum WalletPrefix {
WALLET = 'wallet',
REWARDS_HISTORY = 'rewards_history',
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as Joi from "joi";

export const GetWalletRewardsHistorySchema = Joi.object({
address: Joi.string().required(),
})
34 changes: 34 additions & 0 deletions src/modules/wallet/wallet-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from "@nestjs/common";

import { RedisService } from "@core/lib/redis.service";
import { config } from "@core/config/config";
import { WalletPrefix } from "./enums/wallet-prefix.enum";

@Injectable()
export class WalletCache {
constructor(
private readonly redisService: RedisService,
) { }

async setWalletRewardHistory(hash: string, history: unknown) {
await this.redisService.setWithTTL(this.createRedisKey(WalletPrefix.REWARDS_HISTORY, hash), JSON.stringify(history), config.cache.walletRewardHistory);
}

async getWalletRewardHistory(hash: string) {
return this.getObjectFromRedis(this.createRedisKey(WalletPrefix.REWARDS_HISTORY, hash));
}

private async getObjectFromRedis<T>(key: string): Promise<T | null> {
const serialized = await this.redisService.get(key);

if (!serialized) {
return null;
}

return JSON.parse(serialized as string);
}

private createRedisKey(...ids: string[]) {
return ids.reduce((acc, id) => acc + `_${id}`, `${WalletPrefix.WALLET}`);
}
}
1 change: 1 addition & 0 deletions src/modules/wallet/wallet-routes.enum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum WalletRoutesEnum {
BALANCES = 'balances',
REWARD_HISTORY = 'reward-history',
}
14 changes: 12 additions & 2 deletions src/modules/wallet/wallet.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Routes } from "@core/enums/routes.enum";
import { SchemaValidatePipe } from "@core/pipes/schema-validate.pipe";
import { Controller, Get, Query } from "@nestjs/common";
import { GetBalancesSchema } from "./get-balances.schema";
import { GetBalancesDto } from "./get-balances.dto";
import { GetBalancesSchema } from "./schemas/get-balances.schema";
import { GetBalancesDto } from "./dtos/get-balances.dto";
import { WalletRoutesEnum } from "./wallet-routes.enum";
import { WalletService } from "./wallet.service";
import { GetWalletRewardsHistoryDto } from "./dtos/get-wallet-rewards-history.dto";
import { GetWalletRewardsHistorySchema } from "./schemas/get-wallet-rewards-history.schema";

@Controller(Routes.WALLET)
export class WalletController {
Expand All @@ -17,4 +19,12 @@ export class WalletController {
) {
return this.service.getBalances(dto);
}

@Get(WalletRoutesEnum.REWARD_HISTORY)
async getWalletRewardsHistory(
@Query(new SchemaValidatePipe(GetWalletRewardsHistorySchema))
dto: GetWalletRewardsHistoryDto
) {
return this.service.getWalletRewardsHistory(dto);
}
}
4 changes: 3 additions & 1 deletion src/modules/wallet/wallet.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Okp4Service } from "@core/lib/okp4/okp4.service";
import { Module } from "@nestjs/common";
import { WalletController } from "./wallet.controller";
import { WalletService } from "./wallet.service";
import { WalletCache } from "./wallet-cache";
import { RedisService } from "@core/lib/redis.service";

@Module({
imports: [],
providers: [WalletService, Okp4Service, HttpService],
providers: [WalletService, Okp4Service, HttpService, WalletCache, RedisService],
controllers: [WalletController],
})
export class WalletModule {}
48 changes: 46 additions & 2 deletions src/modules/wallet/wallet.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Okp4Service } from "@core/lib/okp4/okp4.service";
import { Injectable } from "@nestjs/common";
import { GetBalancesDto } from "./get-balances.dto";
import { GetBalancesDto } from "./dtos/get-balances.dto";
import { GetWalletRewardsHistoryDto } from "./dtos/get-wallet-rewards-history.dto";
import { Tx } from "@core/lib/okp4/responses/rewards-history.response";
import { extractNumbers } from "@utils/exctract-numbers";
import { WalletCache } from "./wallet-cache";
import { hash } from "@utils/create-hash";

@Injectable()
export class WalletService {
constructor(private readonly okp4Service: Okp4Service) {}
constructor(private readonly okp4Service: Okp4Service, private readonly cache: WalletCache) {}

async getBalances(payload: GetBalancesDto) {
const res = await this.okp4Service.getBalances(
Expand All @@ -22,4 +27,43 @@ export class WalletService {
},
};
}

async getWalletRewardsHistory(payload: GetWalletRewardsHistoryDto) {
const cache = await this.cache.getWalletRewardHistory(hash(payload));

if(!cache) {
return this.fetchAndCacheRewardsHistory(payload);
}

return cache;
}

private async fetchAndCacheRewardsHistory({ address, limit, offset }: GetWalletRewardsHistoryDto) {
const res = await this.okp4Service.getWalletRewardsHistory(address, limit, offset);
const historyView = res.tx_responses.map(tx => this.walletRewardHistoryView(tx));
await this.cache.setWalletRewardHistory(hash({ address, limit, offset }), historyView);
return historyView;
}

private walletRewardHistoryView(tx: Tx) {
const coinSpendEvent = tx.events.find(event => event.type === 'coin_spent');
const messages = tx.tx.body.messages.map(message => {
const splitted = message["@type"].split('.');
return splitted[splitted.length - 1];
});
let amount = 0;

if(coinSpendEvent) {
const amountAttribute = coinSpendEvent.attributes.find(attribute => attribute.key === 'amount');
amountAttribute && (amount = extractNumbers(amountAttribute?.value)[0]);
}

return {
txHash: tx.txhash,
result: tx.code ? 'Success' : 'Failed',
messages,
amount,
time: tx.timestamp
}
}
}
5 changes: 5 additions & 0 deletions utils/create-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createHash } from "crypto";

export function hash(obj: unknown): string {
return createHash("sha256").update(JSON.stringify(obj)).digest("hex");
}
7 changes: 7 additions & 0 deletions utils/exctract-numbers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function extractNumbers(string: string): number[] {
const matches = string.match(/\d+/g);
if (matches) {
return matches.map(Number);
}
return [];
}

0 comments on commit be482b8

Please sign in to comment.