Skip to content

Commit 97a73dd

Browse files
authored
Merge pull request #254 from harmony-one/master
Release v0.0.0
2 parents 22e574a + 18bb57d commit 97a73dd

File tree

10 files changed

+969
-167
lines changed

10 files changed

+969
-167
lines changed

package-lock.json

Lines changed: 778 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/database/chat.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class ChatService {
6464
const newAmount = bn(chat.creditAmount).minus(bn(amount))
6565

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

7070
return chatRepository.update({

src/modules/schedule/bridgeAPI.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import moment from 'moment'
33
import {abbreviateNumber, getPercentDiff} from "./utils";
44

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

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

67+
export const getStakingStats = async () => {
68+
const { data } = await axios.get<{ "total-staking": string }>(`${stakeApiUrl}/networks/harmony/network_info_lite`)
69+
return data
70+
}
71+
6672
export const getBridgeStats = async () => {
6773
const daysCount = 7
6874
const weekTimestamp = moment().subtract(daysCount - 1,'days').unix()
@@ -129,3 +135,13 @@ export const getBridgeStats = async () => {
129135
change
130136
}
131137
}
138+
139+
export const getTVL = async () => {
140+
const tokens = await getTokensList()
141+
return tokens.reduce((acc, item) => acc + +item.totalLockedUSD, 0)
142+
}
143+
144+
export const getTotalStakes = async () => {
145+
const { "total-staking": totalStaking } = await getStakingStats()
146+
return Math.round(+totalStaking / 10**18)
147+
}

src/modules/schedule/exchangeApi.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import axios from "axios";
2+
3+
interface CoinGeckoResponse {
4+
harmony: {
5+
usd: string;
6+
};
7+
}
8+
9+
export const getOneRate = async () => {
10+
const { data } = await axios.get<CoinGeckoResponse>(
11+
`https://api.coingecko.com/api/v3/simple/price?ids=harmony&vs_currencies=usd`
12+
);
13+
return +data.harmony.usd;
14+
}

src/modules/schedule/explorerApi.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import axios from 'axios'
22
import config from "../../config";
33
import {abbreviateNumber, getPercentDiff} from "./utils";
44

5+
export interface MetricsDaily {
6+
date: string
7+
value: string
8+
}
9+
10+
const { explorerRestApiUrl: apiUrl, explorerRestApiKey: apiKey } = config.schedule
11+
512
export enum MetricsDailyType {
613
walletsCount = 'wallets_count',
714
transactionsCount = 'transactions_count',
@@ -10,14 +17,7 @@ export enum MetricsDailyType {
1017
totalFee = 'total_fee',
1118
}
1219

13-
export interface MetricsDaily {
14-
date: string
15-
value: string
16-
}
17-
18-
const { explorerRestApiUrl: apiUrl, explorerRestApiKey: apiKey } = config.schedule
19-
20-
const getDailyMetrics = async (type: string, limit: number) => {
20+
export const getDailyMetrics = async (type: MetricsDailyType, limit: number) => {
2121
const feesUrl = `${apiUrl}/v0/metrics?type=${type}&limit=${limit}`
2222
const { data } = await axios.get<MetricsDaily[]>(feesUrl, {
2323
headers: {
@@ -28,7 +28,7 @@ const getDailyMetrics = async (type: string, limit: number) => {
2828
}
2929

3030
export const getFeeStats = async () => {
31-
const metrics = await getDailyMetrics('total_fee', 14)
31+
const metrics = await getDailyMetrics(MetricsDailyType.totalFee, 14)
3232

3333
let feesWeek1 = 0, feesWeek2 = 0
3434

src/modules/schedule/index.ts

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import pino from "pino";
22
import { Bot } from 'grammy'
33
import cron from 'node-cron'
4-
import { LRUCache } from 'lru-cache'
54
import config from '../../config'
65
import {BotContext, OnMessageContext} from "../types";
7-
import {getFeeStats} from "./explorerApi";
6+
import {getDailyMetrics, MetricsDailyType} from "./explorerApi";
87
import {getAddressBalance, getBotFee, getBotFeeStats} from "./harmonyApi";
9-
import {getBridgeStats} from "./bridgeAPI";
8+
import {getTotalStakes, getTVL} from "./bridgeAPI";
109
import {statsService} from "../../database/services";
1110
import {abbreviateNumber} from "./utils";
11+
import {getOneRate} from "./exchangeApi";
12+
import {getTradingVolume} from "./subgraphAPI";
1213

1314
enum SupportedCommands {
1415
BOT_STATS = 'botstats',
@@ -29,9 +30,6 @@ export class BotSchedule {
2930
}
3031
})
3132

32-
private cache = new LRUCache({ max: 100, ttl: 1000 * 60 * 60 * 2 })
33-
private reportMessage = ''
34-
3533
constructor(bot: Bot<BotContext>) {
3634
this.bot = bot
3735

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

4846
}
4947

50-
private async prepareMetricsUpdate(refetchData = false) {
51-
try {
52-
this.logger.info(`Start preparing stats`)
53-
54-
const networkFeeStats = await getFeeStats()
55-
const networkFeesReport = `*${networkFeeStats.value}* ONE (${networkFeeStats.change}%)`
56-
57-
let bridgeStatsReport = this.cache.get('bridge_report') || ''
58-
this.logger.info(`Bridge stats report from cache: "${bridgeStatsReport}"`)
59-
if(refetchData || !bridgeStatsReport) {
60-
const bridgeStats = await getBridgeStats()
61-
bridgeStatsReport = `*${bridgeStats.value}* USD (${bridgeStats.change}%)`
62-
this.cache.set('bridge_report', bridgeStatsReport)
63-
}
64-
65-
const botFeesReport = await this.getBotFeeReport(this.holderAddress);
66-
67-
const reportMessage =
68-
`\nNetwork fees (7-day growth): ${networkFeesReport}` +
69-
`\nBridge flow: ${bridgeStatsReport}` +
70-
`\nBot fees: ${botFeesReport}`
71-
72-
this.logger.info(`Prepared message: "${reportMessage}"`)
73-
this.reportMessage = reportMessage
74-
return reportMessage
75-
} catch (e) {
76-
console.log('### e', e);
77-
this.logger.error(`Cannot get stats: ${(e as Error).message}`)
78-
}
79-
}
80-
8148
private async postMetricsUpdate() {
8249
const scheduleChatId = config.schedule.chatId
8350
if(!scheduleChatId) {
8451
this.logger.error(`Post updates: no chatId defined. Set [SCHEDULE_CHAT_ID] variable.`)
8552
return
8653
}
8754

88-
if(this.reportMessage) {
89-
await this.bot.api.sendMessage(scheduleChatId, this.reportMessage, {
55+
const reportMessage = await this.generateReport()
56+
if(reportMessage) {
57+
await this.bot.api.sendMessage(scheduleChatId, reportMessage, {
9058
parse_mode: "Markdown",
9159
})
92-
this.logger.info(`Daily metrics posted in chat ${scheduleChatId}: ${this.reportMessage}`)
60+
this.logger.info(`Daily metrics posted in chat ${scheduleChatId}: ${reportMessage}`)
61+
} else {
62+
this.logger.error(`Cannot prepare daily /stats message`)
9363
}
9464
}
9565

9666
private async runCronJob() {
97-
cron.schedule('30 17 * * *', () => {
98-
this.prepareMetricsUpdate(true)
99-
}, {
100-
scheduled: true,
101-
timezone: "Europe/Lisbon"
102-
});
103-
10467
cron.schedule('00 18 * * *', () => {
105-
this.logger.info('Posting daily metrics')
68+
this.logger.info('Posting daily metrics...')
10669
this.postMetricsUpdate()
10770
}, {
10871
scheduled: true,
10972
timezone: "Europe/Lisbon"
11073
});
111-
112-
await this.prepareMetricsUpdate()
113-
// await this.postMetricsUpdate()
11474
}
11575

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

12585
public async generateReport() {
12686
const [
87+
networkFeesWeekly,
88+
walletsCountWeekly,
89+
oneRate,
90+
91+
bridgeTVL,
92+
totalStakes,
93+
swapTradingVolume,
94+
12795
balance,
12896
weeklyUsers,
129-
totalSupportedMessages
97+
dailyMessages
13098
] = await Promise.all([
99+
getDailyMetrics(MetricsDailyType.totalFee, 7),
100+
getDailyMetrics(MetricsDailyType.walletsCount, 7),
101+
getOneRate(),
102+
103+
getTVL(),
104+
getTotalStakes(),
105+
getTradingVolume(),
106+
131107
getAddressBalance(this.holderAddress),
132108
statsService.getActiveUsers(7),
133109
statsService.getTotalMessages(1, true)
134110
])
135111

136-
const report = `\nBot fees: *${abbreviateNumber(balance / Math.pow(10, 18))}* ONE` +
137-
`\nWeekly active users: *${abbreviateNumber(weeklyUsers)}*` +
138-
`\nDaily user engagement: *${abbreviateNumber(totalSupportedMessages)}*`
139-
return report;
112+
const networkFeesSum = networkFeesWeekly.reduce((sum, item) => sum + +item.value, 0)
113+
const walletsCountSum = walletsCountWeekly.reduce((sum, item) => sum + +item.value, 0)
114+
const walletsCountAvg = Math.round(walletsCountSum / walletsCountWeekly.length)
115+
116+
const networkUsage =
117+
`- Network 7-day fees, wallets, price: ` +
118+
`*${abbreviateNumber(networkFeesSum)}* ONE, ${abbreviateNumber(walletsCountAvg)}, $${oneRate.toFixed(4)}`
119+
120+
const swapTradingVolumeSum = swapTradingVolume.reduce((sum, item) => sum + Math.round(+item.volumeUSD), 0)
121+
const totalStakeUSD = Math.round(oneRate * totalStakes)
122+
123+
const assetsUpdate =
124+
`- Total assets, swaps, stakes: ` +
125+
`$${abbreviateNumber(bridgeTVL)}, $${abbreviateNumber(swapTradingVolumeSum)}, $${abbreviateNumber(totalStakeUSD)}`
126+
127+
const oneBotMetrics =
128+
`- Bot total earns, weekly users, daily messages: ` +
129+
`*${abbreviateNumber(balance / Math.pow(10, 18))}* ONE` +
130+
`, ${abbreviateNumber(weeklyUsers)}` +
131+
`, ${abbreviateNumber(dailyMessages)}`
132+
133+
return `${networkUsage}\n${assetsUpdate}\n${oneBotMetrics}`;
140134
}
141135

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

201195
if(ctx.hasCommand(SupportedCommands.BOT_STATS)) {
202-
const report = await this.prepareMetricsUpdate()
196+
const report = await this.generateReport()
203197
if(report) {
204198
await ctx.reply(report, {
205199
parse_mode: "Markdown",

src/modules/schedule/subgraphAPI.ts

Lines changed: 15 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,94 +3,36 @@ import config from '../../config'
33
import moment from "moment/moment";
44
import {getPercentDiff} from "./utils";
55

6-
interface SwapToken {
7-
feesUSD: string
8-
}
9-
10-
interface Swap {
11-
timestamp: string
12-
token0: SwapToken
13-
token1: SwapToken
14-
}
15-
16-
interface SubgraphData {
17-
swaps: Swap[]
6+
export interface TradingVolume {
7+
id: string
8+
volumeUSD: string
9+
date: number
1810
}
1911

2012
interface SubgraphResponse {
21-
data: SubgraphData
13+
data: {
14+
uniswapDayDatas: TradingVolume[]
15+
}
2216
}
2317

24-
const generateQuery = (timestamp: number, skip = 0, first = 100) => {
18+
const generateTradingVolumeQuery = (first = 30) => {
2519
return `
2620
query {
27-
swaps(orderBy: timestamp, orderDirection: desc, where: { timestamp_gt: ${timestamp} }, skip: ${skip}, first: ${first}) {
28-
timestamp
29-
token0 {
30-
feesUSD
31-
},
32-
token1 {
33-
feesUSD
34-
}
21+
uniswapDayDatas(orderBy: date, orderDirection: desc, first: ${first}) {
22+
id,
23+
volumeUSD,
24+
date
3525
}
3626
}
3727
`
3828
}
3929

40-
const getSubgraphData = async (timestamp: number, offset = 0, limit = 1000) => {
30+
export const getTradingVolume = async (daysCount = 30): Promise<TradingVolume[]> => {
4131
const { data } = await axios.post<SubgraphResponse>(
4232
config.schedule.swapSubgraphApiUrl,
4333
{
44-
query: generateQuery(timestamp, offset, limit),
34+
query: generateTradingVolumeQuery(daysCount),
4535
},
4636
);
47-
return data.data;
48-
}
49-
50-
export const getSwapFees = async() => {
51-
const daysCount = 7
52-
const weekTimestamp = moment().subtract(daysCount,'days').unix()
53-
const daysAmountMap: Record<string, number> = {}
54-
const chunkSize = 1000
55-
56-
for (let i = 0; i < 20; i++) {
57-
const { swaps } = await getSubgraphData(weekTimestamp, 0, chunkSize)
58-
swaps.forEach(swap => {
59-
const { timestamp, token0, token1 } = swap
60-
const date = moment(+timestamp * 1000).format('YYYYMMDD')
61-
62-
const amountUsd = Number(token0.feesUSD) + Number(token1.feesUSD)
63-
if(daysAmountMap[date]) {
64-
daysAmountMap[date] += amountUsd
65-
} else {
66-
daysAmountMap[date] = amountUsd
67-
}
68-
})
69-
70-
if(swaps.length < chunkSize) {
71-
break;
72-
}
73-
const lastSwap = swaps[swaps.length - 1]
74-
if(lastSwap && +lastSwap.timestamp < weekTimestamp) {
75-
break;
76-
}
77-
}
78-
79-
const daysAmountList = Object.entries(daysAmountMap)
80-
.sort(([a], [b]) => +b - +a)
81-
.map(([_, value]) => Math.round(value))
82-
83-
const realDaysCount = daysAmountList.length
84-
const value = daysAmountList[0] // Latest day
85-
const valueTotal = daysAmountList.reduce((sum, item) => sum += item, 0)
86-
const average = valueTotal / realDaysCount
87-
let change = getPercentDiff(average, value).toFixed(1)
88-
if(+change > 0) {
89-
change = `+${change}`
90-
}
91-
92-
return {
93-
value,
94-
change
95-
}
37+
return data.data.uniswapDayDatas;
9638
}

0 commit comments

Comments
 (0)