Skip to content

Commit

Permalink
Merge pull request #253 from harmony-one/dev
Browse files Browse the repository at this point in the history
Introducing new structure and versioning.
  • Loading branch information
theofandrich authored Sep 7, 2023
2 parents 22e574a + 3c70d4e commit b824336
Show file tree
Hide file tree
Showing 8 changed files with 892 additions and 166 deletions.
796 changes: 778 additions & 18 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/database/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class ChatService {
const newAmount = bn(chat.creditAmount).minus(bn(amount))

if(newAmount.lt(0)) {
throw new Error(`${accountId} Insufficient credits: cannot withdraw ${amount}, current balance ${chat.creditAmount}`)
throw new Error(`${accountId} Insufficient credits: can not withdraw ${amount}, current balance ${chat.creditAmount}`)
}

return chatRepository.update({
Expand Down
16 changes: 16 additions & 0 deletions src/modules/schedule/bridgeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import moment from 'moment'
import {abbreviateNumber, getPercentDiff} from "./utils";

const bridgeUrl = 'https://hmy-lz-api-token.fly.dev'
const stakeApiUrl = 'https://api.stake.hmny.io'

interface BridgeOperation {
id: number
Expand Down Expand Up @@ -63,6 +64,11 @@ export const getTokensList = async (): Promise<BridgeToken[]> => {
return data.content
}

export const getStakingStats = async () => {
const { data } = await axios.get<{ "total-staking": string }>(`${stakeApiUrl}/networks/harmony/network_info_lite`)
return data
}

export const getBridgeStats = async () => {
const daysCount = 7
const weekTimestamp = moment().subtract(daysCount - 1,'days').unix()
Expand Down Expand Up @@ -129,3 +135,13 @@ export const getBridgeStats = async () => {
change
}
}

export const getTVL = async () => {
const tokens = await getTokensList()
return tokens.reduce((acc, item) => acc + +item.totalLockedUSD, 0)
}

export const getTotalStakes = async () => {
const { "total-staking": totalStaking } = await getStakingStats()
return Math.round(+totalStaking / 10**18)
}
14 changes: 14 additions & 0 deletions src/modules/schedule/exchangeApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axios from "axios";

interface CoinGeckoResponse {
harmony: {
usd: string;
};
}

export const getOneRate = async () => {
const { data } = await axios.get<CoinGeckoResponse>(
`https://api.coingecko.com/api/v3/simple/price?ids=harmony&vs_currencies=usd`
);
return +data.harmony.usd;
}
18 changes: 9 additions & 9 deletions src/modules/schedule/explorerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import axios from 'axios'
import config from "../../config";
import {abbreviateNumber, getPercentDiff} from "./utils";

export interface MetricsDaily {
date: string
value: string
}

const { explorerRestApiUrl: apiUrl, explorerRestApiKey: apiKey } = config.schedule

export enum MetricsDailyType {
walletsCount = 'wallets_count',
transactionsCount = 'transactions_count',
Expand All @@ -10,14 +17,7 @@ export enum MetricsDailyType {
totalFee = 'total_fee',
}

export interface MetricsDaily {
date: string
value: string
}

const { explorerRestApiUrl: apiUrl, explorerRestApiKey: apiKey } = config.schedule

const getDailyMetrics = async (type: string, limit: number) => {
export const getDailyMetrics = async (type: MetricsDailyType, limit: number) => {
const feesUrl = `${apiUrl}/v0/metrics?type=${type}&limit=${limit}`
const { data } = await axios.get<MetricsDaily[]>(feesUrl, {
headers: {
Expand All @@ -28,7 +28,7 @@ const getDailyMetrics = async (type: string, limit: number) => {
}

export const getFeeStats = async () => {
const metrics = await getDailyMetrics('total_fee', 14)
const metrics = await getDailyMetrics(MetricsDailyType.totalFee, 14)

let feesWeek1 = 0, feesWeek2 = 0

Expand Down
108 changes: 51 additions & 57 deletions src/modules/schedule/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import pino from "pino";
import { Bot } from 'grammy'
import cron from 'node-cron'
import { LRUCache } from 'lru-cache'
import config from '../../config'
import {BotContext, OnMessageContext} from "../types";
import {getFeeStats} from "./explorerApi";
import {getDailyMetrics, MetricsDailyType} from "./explorerApi";
import {getAddressBalance, getBotFee, getBotFeeStats} from "./harmonyApi";
import {getBridgeStats} from "./bridgeAPI";
import {getTotalStakes, getTVL} from "./bridgeAPI";
import {statsService} from "../../database/services";
import {abbreviateNumber} from "./utils";
import {getOneRate} from "./exchangeApi";
import {getTradingVolume} from "./subgraphAPI";

enum SupportedCommands {
BOT_STATS = 'botstats',
Expand All @@ -29,9 +30,6 @@ export class BotSchedule {
}
})

private cache = new LRUCache({ max: 100, ttl: 1000 * 60 * 60 * 2 })
private reportMessage = ''

constructor(bot: Bot<BotContext>) {
this.bot = bot

Expand All @@ -47,70 +45,32 @@ export class BotSchedule {

}

private async prepareMetricsUpdate(refetchData = false) {
try {
this.logger.info(`Start preparing stats`)

const networkFeeStats = await getFeeStats()
const networkFeesReport = `*${networkFeeStats.value}* ONE (${networkFeeStats.change}%)`

let bridgeStatsReport = this.cache.get('bridge_report') || ''
this.logger.info(`Bridge stats report from cache: "${bridgeStatsReport}"`)
if(refetchData || !bridgeStatsReport) {
const bridgeStats = await getBridgeStats()
bridgeStatsReport = `*${bridgeStats.value}* USD (${bridgeStats.change}%)`
this.cache.set('bridge_report', bridgeStatsReport)
}

const botFeesReport = await this.getBotFeeReport(this.holderAddress);

const reportMessage =
`\nNetwork fees (7-day growth): ${networkFeesReport}` +
`\nBridge flow: ${bridgeStatsReport}` +
`\nBot fees: ${botFeesReport}`

this.logger.info(`Prepared message: "${reportMessage}"`)
this.reportMessage = reportMessage
return reportMessage
} catch (e) {
console.log('### e', e);
this.logger.error(`Cannot get stats: ${(e as Error).message}`)
}
}

private async postMetricsUpdate() {
const scheduleChatId = config.schedule.chatId
if(!scheduleChatId) {
this.logger.error(`Post updates: no chatId defined. Set [SCHEDULE_CHAT_ID] variable.`)
return
}

if(this.reportMessage) {
await this.bot.api.sendMessage(scheduleChatId, this.reportMessage, {
const reportMessage = await this.generateReport()
if(reportMessage) {
await this.bot.api.sendMessage(scheduleChatId, reportMessage, {
parse_mode: "Markdown",
})
this.logger.info(`Daily metrics posted in chat ${scheduleChatId}: ${this.reportMessage}`)
this.logger.info(`Daily metrics posted in chat ${scheduleChatId}: ${reportMessage}`)
} else {
this.logger.error(`Cannot prepare daily /stats message`)
}
}

private async runCronJob() {
cron.schedule('30 17 * * *', () => {
this.prepareMetricsUpdate(true)
}, {
scheduled: true,
timezone: "Europe/Lisbon"
});

cron.schedule('00 18 * * *', () => {
this.logger.info('Posting daily metrics')
this.logger.info('Posting daily metrics...')
this.postMetricsUpdate()
}, {
scheduled: true,
timezone: "Europe/Lisbon"
});

await this.prepareMetricsUpdate()
// await this.postMetricsUpdate()
}

public isSupportedEvent(ctx: OnMessageContext) {
Expand All @@ -124,19 +84,53 @@ export class BotSchedule {

public async generateReport() {
const [
networkFeesWeekly,
walletsCountWeekly,
oneRate,

bridgeTVL,
totalStakes,
swapTradingVolume,

balance,
weeklyUsers,
totalSupportedMessages
dailyMessages
] = await Promise.all([
getDailyMetrics(MetricsDailyType.totalFee, 7),
getDailyMetrics(MetricsDailyType.walletsCount, 7),
getOneRate(),

getTVL(),
getTotalStakes(),
getTradingVolume(),

getAddressBalance(this.holderAddress),
statsService.getActiveUsers(7),
statsService.getTotalMessages(1, true)
])

const report = `\nBot fees: *${abbreviateNumber(balance / Math.pow(10, 18))}* ONE` +
`\nWeekly active users: *${abbreviateNumber(weeklyUsers)}*` +
`\nDaily user engagement: *${abbreviateNumber(totalSupportedMessages)}*`
return report;
const networkFeesSum = networkFeesWeekly.reduce((sum, item) => sum + +item.value, 0)
const walletsCountSum = walletsCountWeekly.reduce((sum, item) => sum + +item.value, 0)
const walletsCountAvg = Math.round(walletsCountSum / walletsCountWeekly.length)

const networkUsage =
`- Network 7-day fees, wallets, price: ` +
`*${abbreviateNumber(networkFeesSum)}* ONE, ${abbreviateNumber(walletsCountAvg)}, $${oneRate.toFixed(4)}`

const swapTradingVolumeSum = swapTradingVolume.reduce((sum, item) => sum + Math.round(+item.volumeUSD), 0)
const totalStakeUSD = Math.round(oneRate * totalStakes)

const assetsUpdate =
`- Total assets, swaps, stakes: ` +
`$${abbreviateNumber(bridgeTVL)}, $${abbreviateNumber(swapTradingVolumeSum)}, $${abbreviateNumber(totalStakeUSD)}`

const oneBotMetrics =
`- Bot total earns, weekly users, daily messages: ` +
`*${abbreviateNumber(balance / Math.pow(10, 18))}* ONE` +
`, ${abbreviateNumber(weeklyUsers)}` +
`, ${abbreviateNumber(dailyMessages)}`

return `${networkUsage}\n${assetsUpdate}\n${oneBotMetrics}`;
}

public async generateReportEngagementByCommand(days: number) {
Expand Down Expand Up @@ -199,7 +193,7 @@ export class BotSchedule {
const { message_id } = ctx.update.message

if(ctx.hasCommand(SupportedCommands.BOT_STATS)) {
const report = await this.prepareMetricsUpdate()
const report = await this.generateReport()
if(report) {
await ctx.reply(report, {
parse_mode: "Markdown",
Expand Down
88 changes: 15 additions & 73 deletions src/modules/schedule/subgraphAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,36 @@ import config from '../../config'
import moment from "moment/moment";
import {getPercentDiff} from "./utils";

interface SwapToken {
feesUSD: string
}

interface Swap {
timestamp: string
token0: SwapToken
token1: SwapToken
}

interface SubgraphData {
swaps: Swap[]
export interface TradingVolume {
id: string
volumeUSD: string
date: number
}

interface SubgraphResponse {
data: SubgraphData
data: {
uniswapDayDatas: TradingVolume[]
}
}

const generateQuery = (timestamp: number, skip = 0, first = 100) => {
const generateTradingVolumeQuery = (first = 30) => {
return `
query {
swaps(orderBy: timestamp, orderDirection: desc, where: { timestamp_gt: ${timestamp} }, skip: ${skip}, first: ${first}) {
timestamp
token0 {
feesUSD
},
token1 {
feesUSD
}
uniswapDayDatas(orderBy: date, orderDirection: desc, first: ${first}) {
id,
volumeUSD,
date
}
}
`
}

const getSubgraphData = async (timestamp: number, offset = 0, limit = 1000) => {
export const getTradingVolume = async (daysCount = 30): Promise<TradingVolume[]> => {
const { data } = await axios.post<SubgraphResponse>(
config.schedule.swapSubgraphApiUrl,
{
query: generateQuery(timestamp, offset, limit),
query: generateTradingVolumeQuery(daysCount),
},
);
return data.data;
}

export const getSwapFees = async() => {
const daysCount = 7
const weekTimestamp = moment().subtract(daysCount,'days').unix()
const daysAmountMap: Record<string, number> = {}
const chunkSize = 1000

for (let i = 0; i < 20; i++) {
const { swaps } = await getSubgraphData(weekTimestamp, 0, chunkSize)
swaps.forEach(swap => {
const { timestamp, token0, token1 } = swap
const date = moment(+timestamp * 1000).format('YYYYMMDD')

const amountUsd = Number(token0.feesUSD) + Number(token1.feesUSD)
if(daysAmountMap[date]) {
daysAmountMap[date] += amountUsd
} else {
daysAmountMap[date] = amountUsd
}
})

if(swaps.length < chunkSize) {
break;
}
const lastSwap = swaps[swaps.length - 1]
if(lastSwap && +lastSwap.timestamp < weekTimestamp) {
break;
}
}

const daysAmountList = Object.entries(daysAmountMap)
.sort(([a], [b]) => +b - +a)
.map(([_, value]) => Math.round(value))

const realDaysCount = daysAmountList.length
const value = daysAmountList[0] // Latest day
const valueTotal = daysAmountList.reduce((sum, item) => sum += item, 0)
const average = valueTotal / realDaysCount
let change = getPercentDiff(average, value).toFixed(1)
if(+change > 0) {
change = `+${change}`
}

return {
value,
change
}
return data.data.uniswapDayDatas;
}
Loading

0 comments on commit b824336

Please sign in to comment.