diff --git a/.env.devnet b/.env.devnet index c562a53..bbee777 100644 --- a/.env.devnet +++ b/.env.devnet @@ -2,6 +2,7 @@ NETWORK=mainnet API_URL=https://api.multiversx.com DATA_API_CEX_URL=https://data-api.multiversx.com/v1/quotes/cex DATA_API_XEXCHANGE_URL=https://data-api.multiversx.com/v1/quotes/xexchange +DATA_API_HATOM_URL=https://data-api.multiversx.com/v1/quotes/hatom # DUNE_API_URL=http://localhost:3001/api/v1/table DUNE_API_URL=https://api.dune.com/api/v1/table DUNE_NAMESPACE=stefanmvx diff --git a/.env.mainnet b/.env.mainnet index aacbf4d..59b77d9 100644 --- a/.env.mainnet +++ b/.env.mainnet @@ -2,6 +2,7 @@ NETWORK=mainnet API_URL=https://api.multiversx.com DATA_API_CEX_URL=https://data-api.multiversx.com/v1/quotes/cex DATA_API_XEXCHANGE_URL=https://data-api.multiversx.com/v1/quotes/xexchange +DATA_API_HATOM_URL=https://data-api.multiversx.com/v1/quotes/hatom DUNE_API_URL=http://localhost:3001/api/v1/table # DUNE_API_URL=https://api.dune.com/api/v1/table DUNE_NAMESPACE=stefanmvx diff --git a/.env.testnet b/.env.testnet index c562a53..bbee777 100644 --- a/.env.testnet +++ b/.env.testnet @@ -2,6 +2,7 @@ NETWORK=mainnet API_URL=https://api.multiversx.com DATA_API_CEX_URL=https://data-api.multiversx.com/v1/quotes/cex DATA_API_XEXCHANGE_URL=https://data-api.multiversx.com/v1/quotes/xexchange +DATA_API_HATOM_URL=https://data-api.multiversx.com/v1/quotes/hatom # DUNE_API_URL=http://localhost:3001/api/v1/table DUNE_API_URL=https://api.dune.com/api/v1/table DUNE_NAMESPACE=stefanmvx diff --git a/.multiversx/config/config.yaml b/.multiversx/config/config.yaml index 316c46e..e06a5da 100644 --- a/.multiversx/config/config.yaml +++ b/.multiversx/config/config.yaml @@ -14,6 +14,7 @@ libs: api: ${API_URL} dataApiCex: ${DATA_API_CEX_URL} dataApiXexchange: ${DATA_API_XEXCHANGE_URL} + dataApiHatom: ${DATA_API_HATOM_URL} duneApi: ${DUNE_API_URL} database: host: 'localhost' diff --git a/apps/api/src/config/app-config.service.ts b/apps/api/src/config/app-config.service.ts index 58d59cd..ec4e5e9 100644 --- a/apps/api/src/config/app-config.service.ts +++ b/apps/api/src/config/app-config.service.ts @@ -25,6 +25,10 @@ export class AppConfigService { return configuration().libs.common.urls.dataApiXexchange ?? ""; } + getDataApiHatomUrl(): string { + return configuration().libs.common.urls.dataApiHatom ?? ""; + } + getDuneApiUrl(): string { return configuration().libs.common.urls.duneApi ?? ""; } diff --git a/config/config.yaml b/config/config.yaml index 316c46e..e06a5da 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -14,6 +14,7 @@ libs: api: ${API_URL} dataApiCex: ${DATA_API_CEX_URL} dataApiXexchange: ${DATA_API_XEXCHANGE_URL} + dataApiHatom: ${DATA_API_HATOM_URL} duneApi: ${DUNE_API_URL} database: host: 'localhost' diff --git a/config/schema.yaml b/config/schema.yaml index 23820ff..4b45872 100644 --- a/config/schema.yaml +++ b/config/schema.yaml @@ -17,6 +17,7 @@ libs: api: string dataApiCex: string dataApiXexchange: string + dataApiHatom: string duneApi: string database: host: string diff --git a/libs/common/src/entities/config.d.ts b/libs/common/src/entities/config.d.ts index 5c61796..53bb019 100644 --- a/libs/common/src/entities/config.d.ts +++ b/libs/common/src/entities/config.d.ts @@ -20,6 +20,7 @@ export interface Config { api: string; dataApiCex: string; dataApiXexchange: string; + dataApiHatom: string; duneApi: string; }; database: { diff --git a/libs/services/src/data/data.service.ts b/libs/services/src/data/data.service.ts index ca45c4f..c3bee24 100644 --- a/libs/services/src/data/data.service.ts +++ b/libs/services/src/data/data.service.ts @@ -23,16 +23,28 @@ export class DataService { private readonly appConfigService: AppConfigService, ) { } - async getTokenPrice(tokenId: string, date: moment.Moment): Promise { + async getTokenPrice(tokenId: string, date: moment.Moment, market?: string): Promise { return await this.cachingService.getOrSet( CacheInfo.TokenPrice(tokenId, date).key, - async () => await this.getTokenPriceRaw(tokenId, date), + async () => await this.getTokenPriceRaw(tokenId, date, market), CacheInfo.TokenPrice(tokenId, date).ttl ); } - async getTokenPriceRaw(tokenId: string, date: moment.Moment): Promise { + async getTokenPriceRaw(tokenId: string, date: moment.Moment, market?: string): Promise { try { + if (market) { + switch (market) { + case 'hatom': + return (await axios.get(`${this.appConfigService.getDataApiHatomUrl()}/${tokenId}?date=${date.format('YYYY-MM-DD')}`)).data.price; + case 'xexchange': + return (await axios.get(`${this.appConfigService.getDataApiXexchangeUrl()}/${tokenId}?date=${date.format('YYYY-MM-DD')}`)).data.price; + case 'cex': + return (await axios.get(`${this.appConfigService.getDataApiCexUrl()}/${tokenId}?date=${date.format('YYYY-MM-DD')}`)).data.price; + default: + throw Error('Invalid market !'); + } + } if (tokenId.startsWith('USD')) { return (await axios.get(`${this.appConfigService.getDataApiCexUrl()}/${tokenId}?date=${date.format('YYYY-MM-DD')}`)).data.price; } diff --git a/libs/services/src/events/hatom.borrow.events.service.ts b/libs/services/src/events/hatom.borrow.events.service.ts index 006c2d3..e2dc7a3 100644 --- a/libs/services/src/events/hatom.borrow.events.service.ts +++ b/libs/services/src/events/hatom.borrow.events.service.ts @@ -5,8 +5,7 @@ import { CsvRecordsService } from "../records"; import moment from "moment"; import { DataService } from "../data"; import { TableSchema } from "apps/dune-simulator/src/endpoints/dune-simulator/entities"; -import { decodeTopics, HatomEvent, joinCsvAttributes } from "libs/services/utils"; -import { borrowEvent } from "../../utils/hex-constants"; +import { borrowEvent, decodeTopics, HatomEvent, joinCsvAttributes } from "libs/services/utils"; interface BorrowEvent extends HatomEvent { eventName: string; diff --git a/libs/services/src/events/hatom.liquidation.service.ts b/libs/services/src/events/hatom.liquidation.service.ts index c85970f..c9f01cd 100644 --- a/libs/services/src/events/hatom.liquidation.service.ts +++ b/libs/services/src/events/hatom.liquidation.service.ts @@ -1,26 +1,35 @@ import { Injectable } from "@nestjs/common"; import { TableSchema } from "apps/dune-simulator/src/endpoints/dune-simulator/entities"; import { EventLog } from "apps/api/src/endpoints/events/entities"; -import { liquidationBorrowEvent } from "../../utils/hex-constants"; import { CsvRecordsService } from "../records"; -import { decodeTopics, HatomEvent, joinCsvAttributes } from "libs/services/utils"; +import { liquidationBorrowEvent, decodeTopics, getTokenIdByMoneyMarket, HatomEvent, joinCsvAttributes } from "libs/services/utils"; import BigNumber from "bignumber.js"; import moment from "moment"; +import { DataService } from "../data"; interface LiquidationEvent extends HatomEvent { - 'liquidator': string, - 'borrower': string, - 'amount': BigNumber, - 'collateral_mma': string, - 'tokens': BigNumber, + liquidator: string, + borrower: string, + amount: BigNumber, + collateral_mma: string, + tokens: BigNumber, } @Injectable() export class HatomLiquidationService { - private readonly headers: TableSchema[] = []; + private readonly headers: TableSchema[] = [ + { name: "timestamp", type: "varchar" }, + { name: 'liquidator', type: 'varchar' }, + { name: 'account_liquidated', type: 'varchar' }, + { name: 'token', type: 'varchar' }, + { name: 'amount', type: 'double' }, + { name: 'amount_in_egld', type: 'double' }, + { name: 'amount_in_usd', type: 'double' }, + ]; constructor( private readonly csvRecordsService: CsvRecordsService, + private readonly dataService: DataService, ) {} public async hatomLiquidationWebhook(eventsLog: EventLog[]): Promise { @@ -31,17 +40,42 @@ export class HatomLiquidationService { const currentEvent: LiquidationEvent = decodeTopics(properties, eventLog.topics.slice(1), types) as LiquidationEvent; const eventDate = moment.unix(eventLog.timestamp); - await this.csvRecordsService.pushRecord("hatom_liquidation_events", [ - joinCsvAttributes( - currentEvent.liquidator, - currentEvent.borrower, - eventDate.format('YYYY-MM-DD HH:mm:ss.SSS'), - currentEvent.amount.shiftedBy(-18).decimalPlaces(4), - currentEvent.collateral_mma, - currentEvent.tokens.shiftedBy(-18).decimalPlaces(4), - ), - ], this.headers); + const tokenId: string = getTokenIdByMoneyMarket(currentEvent.collateral_mma); + + if (tokenId === "Not Found") { + console.log("Token ID not found for money market:", currentEvent.collateral_mma); + continue; + } + + const tokenPrecision = await this.dataService.getTokenPrecision(tokenId); + const [liquidatedAmountInEGLD, liquidatedAmountInUSD] = await this.convertTokenValue(currentEvent.tokens, tokenId, eventDate); + + await this.csvRecordsService.pushRecord( + "hatom_liquidation_events", + [ + joinCsvAttributes( + eventDate.format('YYYY-MM-DD HH:mm:ss.SSS'), + currentEvent.liquidator, + currentEvent.borrower, + tokenId, + currentEvent.tokens.shiftedBy(-tokenPrecision).decimalPlaces(4), + liquidatedAmountInEGLD.shiftedBy(-tokenPrecision).decimalPlaces(4), + liquidatedAmountInUSD.shiftedBy(-tokenPrecision).decimalPlaces(4), + ), + ], + this.headers + ); } } } + + async convertTokenValue(amount: BigNumber, tokenID: string, date: moment.Moment): Promise<[BigNumber, BigNumber]> { + const egldPrice = await this.dataService.getTokenPrice('WEGLD-bd4d79', date); + const tokenPrice = await this.dataService.getTokenPrice(tokenID, date, 'hatom'); + + const valueInUsd = amount.multipliedBy(tokenPrice); + const valueInEgld = valueInUsd.dividedBy(egldPrice); + + return [valueInEgld, valueInUsd]; + } }